Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code 2024 - Day 12

2024/day-12.livemd

Advent of Code 2024 - Day 12

Mix.install([
  {:kino_aoc, "~> 0.1.7"}
])

Section

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "12", System.fetch_env!("LB_AOC_SESSION"))
grid =
  puzzle_input
  |> String.split("\n")
  |> Enum.map(&String.to_charlist/1)
garden =
  for {row, i} <- Enum.with_index(grid),
      {plant, j} <- Enum.with_index(row),
      into: %{},
      do: {{i, j}, plant}
defmodule AoC2024.Day12 do
  def regions(garden) do
    garden
    |> regions([])
    |> Enum.map(&amp;MapSet.new/1)
  end

  def border_tiles(region) do
    Enum.filter(region, fn tile ->
      tile
      |> neighbors()
      |> Enum.any?(&amp; &amp;1 not in region)
    end)
  end

  def area(region) do
    MapSet.size(region)
  end

  def perimeter(region) do    
    region
    |> border_tiles()
    |> Enum.map(fn tile ->
      tile
      |> neighbors()
      |> Enum.count(&amp; &amp;1 not in region)
    end)
    |> Enum.sum()
  end

  def sides(region) do
    border_tiles =
      region
      |> border_tiles()
      |> Enum.flat_map(fn tile ->
        acc = (up(tile) in region) &amp;&amp; [] || [{tile, :up}]
        acc = (down(tile) in region) &amp;&amp; acc || [{tile, :down} | acc]
        acc = (left(tile) in region) &amp;&amp; acc || [{tile, :left} | acc]
        (right(tile) in region) &amp;&amp; acc || [{tile, :right} | acc]
      end)
      |> MapSet.new()

    count_sides(border_tiles, 0)
  end

  defp count_sides(border_tiles, acc) do
    if MapSet.size(border_tiles) == 0 do
      acc
    else
      border_tiles = remove_border(border_tiles, Enum.min(border_tiles))
      count_sides(border_tiles, acc + 1)
    end
  end

  defp remove_border(border_tiles, {tile, dir} = entry) when dir in [:up, :down] do
    if entry in border_tiles do
      border_tiles = MapSet.delete(border_tiles, entry)
      remove_border(border_tiles, {right(tile), dir})
    else
      border_tiles
    end
  end

  defp remove_border(border_tiles, {tile, dir} = entry) when dir in [:left, :right] do
    if entry in border_tiles do
      border_tiles = MapSet.delete(border_tiles, entry)
      remove_border(border_tiles, {down(tile), dir})
    else
      border_tiles
    end
  end

  defp neighbors(tile) do
    [
      up(tile),
      down(tile),
      left(tile),
      right(tile)
    ]
  end

  defp up({i, j}) do
    {i - 1, j}
  end

  defp down({i, j}) do
    {i + 1, j}
  end

  defp left({i, j}) do
    {i, j - 1}
  end

  defp right({i, j}) do
    {i, j + 1}
  end

  defp regions(garden, acc) when map_size(garden) == 0 do
    acc
  end
  
  defp regions(garden, acc) do
    {tile, plant} = Enum.at(garden, 0)
    {region, garden} = dfs_region(garden, plant, tile, [])
    regions(garden, [region | acc])
  end

  defp dfs_region(garden, plant, tile, acc) do
    if garden[tile] == plant do
      garden = Map.delete(garden, tile)
      acc = [tile | acc]
      
      for neighbor <- neighbors(tile), reduce: {acc, garden} do
        {acc, garden} -> dfs_region(garden, plant, neighbor, acc)
      end
    else
      {acc, garden}
    end
  end
end
import AoC2024.Day12
regions = regions(garden)
for region <- regions, reduce: 0 do
  total -> total + area(region) * perimeter(region)
end
for region <- regions, reduce: 0 do
  total -> total + area(region) * sides(region)
end