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, github: "livebook-dev/kino"}])

kino_input = Kino.Input.textarea("Please paste your input file: ")

Part 1

input = Kino.Input.read(kino_input)

defmodule Garden do
  def regions(garden_map) do
    garden_map
    |> compute_regions([])
    |> Enum.map(&MapSet.new/1)
  end

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

  def perimeter(region) do    
    region
    |> Enum.filter(fn tile ->
      Enum.any?(
        [
          {elem(tile, 0) - 1, elem(tile, 1)},  # up
          {elem(tile, 0) + 1, elem(tile, 1)},  # down
          {elem(tile, 0), elem(tile, 1) - 1},  # left
          {elem(tile, 0), elem(tile, 1) + 1}   # right
        ],
        &(&1 not in region)
      )
    end)
    |> Enum.map(fn tile ->
      Enum.count(
        [
          {elem(tile, 0) - 1, elem(tile, 1)},  # up
          {elem(tile, 0) + 1, elem(tile, 1)},  # down
          {elem(tile, 0), elem(tile, 1) - 1},  # left
          {elem(tile, 0), elem(tile, 1) + 1}   # right
        ],
        &(&1 not in region)
      )
    end)
    |> Enum.sum()
  end

  defp compute_regions(garden_map, acc) when map_size(garden_map) == 0 do
    acc
  end

  defp compute_regions(garden_map, acc) do
    {coords, plant} = Enum.at(garden_map, 0)
    {region, garden} = search_region(garden_map, plant, coords, [])
    
    compute_regions(garden, [region | acc])
  end

  defp search_region(garden, plant, tile, acc) do
    if garden[tile] == plant do
      garden = Map.delete(garden, tile)
      acc = [tile | acc]
      
      neighbors = [
      {elem(tile, 0) - 1, elem(tile, 1)},  # up
      {elem(tile, 0) + 1, elem(tile, 1)},  # down
      {elem(tile, 0), elem(tile, 1) - 1},  # left
      {elem(tile, 0), elem(tile, 1) + 1}   # right
    ]

      Enum.reduce(neighbors, {acc, garden}, fn neighbor, {acc, garden} ->
        search_region(garden, plant, neighbor, acc)
      end)
    else
      {acc, garden}
    end
  end
end

input
|> String.split("\n", trim: true)
|> Enum.map(&String.to_charlist/1)
|> Enum.with_index()
|> Enum.reduce(%{}, fn {row, row_index}, acc ->
  row
  |> Enum.with_index()
  |> Enum.reduce(acc, fn {plant, column_index}, inner_acc ->
    Map.put(inner_acc, {row_index, column_index}, plant)
  end)
end)
|> Garden.regions()
|> Enum.reduce(0, fn region, acc ->
  acc + Garden.area(region) * Garden.perimeter(region)
end)

Part 2

input = Kino.Input.read(kino_input)

defmodule Garden2 do
  def regions(garden_map) do
    garden_map
    |> compute_regions([])
    |> Enum.map(&MapSet.new/1)
  end

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

  def sides(region) do
    border_tiles =
      region
      |> border_tiles()
      |> Enum.flat_map(fn tile ->
        {y, x} = tile
        
        acc = if {y - 1, x} in region, do: [], else: [{tile, :up}]
        acc = if {y + 1, x} in region, do: acc, else: [{tile, :down} | acc]
        acc = if {y, x - 1} in region, do: acc, else: [{tile, :left} | acc]
        if {y, x + 1} in region, do: acc, else: [{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
      {tile, dir} = Enum.min(border_tiles)
      border_tiles = remove_border(border_tiles, {tile, dir})
      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)
      {y, x} = tile
      remove_border(border_tiles, {{y, x + 1}, 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)
      {y, x} = tile
      remove_border(border_tiles, {{y + 1, x}, dir})
    else
      border_tiles
    end
  end

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

  defp neighbors({i, j}) do
    [
      {i - 1, j},  # up
      {i + 1, j},  # down
      {i, j - 1},  # left
      {i, j + 1}   # right
    ]
  end

  defp compute_regions(garden_map, acc) when map_size(garden_map) == 0 do
    acc
  end

  defp compute_regions(garden_map, acc) do
    {coords, plant} = Enum.at(garden_map, 0)
    {region, garden} = search_region(garden_map, plant, coords, [])
    
    compute_regions(garden, [region | acc])
  end

  defp search_region(garden, plant, tile, acc) do
    if garden[tile] == plant do
      garden = Map.delete(garden, tile)
      acc = [tile | acc]
      
      neighbors = [
      {elem(tile, 0) - 1, elem(tile, 1)},  # up
      {elem(tile, 0) + 1, elem(tile, 1)},  # down
      {elem(tile, 0), elem(tile, 1) - 1},  # left
      {elem(tile, 0), elem(tile, 1) + 1}   # right
    ]

      Enum.reduce(neighbors, {acc, garden}, fn neighbor, {acc, garden} ->
        search_region(garden, plant, neighbor, acc)
      end)
    else
      {acc, garden}
    end
  end
end

input
|> String.split("\n", trim: true)
|> Enum.map(&String.to_charlist/1)
|> Enum.with_index()
|> Enum.reduce(%{}, fn {row, row_index}, acc ->
  row
  |> Enum.with_index()
  |> Enum.reduce(acc, fn {plant, column_index}, inner_acc ->
    Map.put(inner_acc, {row_index, column_index}, plant)
  end)
end)
|> Garden2.regions()
|> Enum.reduce(0, fn region, acc ->
  acc + Garden2.area(region) * Garden2.sides(region)
end)