Advent of Code 2024 - Day 08
Mix.install([
  {:req, "~> 0.5"},
  {:benchee, "~> 1.3"}
])
Input
opts = [headers: [{"cookie", "session=#{System.fetch_env!("LB_AOC_SESSION")}"}]]
puzzle_input = Req.get!("https://adventofcode.com/2024/day/12/input", opts).body
garden =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.map(&String.graphemes/1)
  |> then(fn grid ->
    for {row, y} <- Enum.with_index(grid), {value, x} <- Enum.with_index(row), into: %{} do
      {{y, x}, value}
    end
  end)
defmodule GardenGroups do
  def groups(garden) do
    Enum.reduce(garden, [], fn {pos, plant}, acc ->
      if pos in List.flatten(acc) do
        acc
      else
        group = find_connected(garden, pos, plant, MapSet.new([])) |> MapSet.to_list()
        [group | acc]
      end
    end)
  end
  def find_connected(garden, pos, plant, positions) do
    if at(garden, pos) == plant and not MapSet.member?(positions, pos) do
      positions = MapSet.put(positions, pos)
      Enum.reduce(dirs(), positions, fn dir, acc ->
        find_connected(garden, move_dir(dir, pos), plant, acc)
      end)
    else
      positions
    end
  end
def count_corners(garden, pos) do
  plant = at(garden, pos)
  [
    {up(pos), left(pos), left(pos) |> up()},
    {up(pos), right(pos), right(pos) |> up()},
    {down(pos), left(pos), left(pos) |> down()},
    {down(pos), right(pos), right(pos) |> down()}
  ]
  |> Enum.count(fn {pos1, pos2, diagonal} ->
    val1 = at(garden, pos1)
    val2 = at(garden, pos2)
    diagonal_val = at(garden, diagonal)
    # Convex corner: both adjacents are different from group
    convex_corner = val1 != plant and val2 != plant
    # Concave corner: both adjacents match group AND diagonal is different
    concave_corner = val1 == plant and val2 == plant and diagonal_val != plant
    convex_corner or concave_corner
  end)
end
  def neighbors(garden, pos) do
    Enum.map(dirs(), fn dir -> at(garden, move_dir(dir, pos)) end)
  end
  def at(garden, pos), do: Map.get(garden, pos)
  def move_dir({dir_y, dir_x} = _dir, {y, x} = _pos), do: {dir_y + y, dir_x + x}
  def dirs, do: [{1, 0}, {0, 1}, {-1, 0}, {0, -1}]
  def up({y, x}), do: {y + 1, x}
  def right({y, x}), do: {y, x + 1}
  def down({y, x}), do: {y - 1, x}
  def left({y, x}), do: {y, x - 1}
end
Puzzle 1
puzzle_1 = fn ->
  GardenGroups.groups(garden)
  |> Task.async_stream(fn group ->
    plant = GardenGroups.at(garden, hd(group))
    fences =
      Enum.flat_map(group, fn pos ->
        GardenGroups.neighbors(garden, pos)
      end)
      |> Enum.filter(&(&1 != plant))
      |> Enum.count()
    fences * length(group)
  end)
  |> Enum.reduce(0, fn {:ok, cost}, acc -> cost + acc end)
end
puzzle_1.()
Puzzle 2
puzzle_2 = fn ->
  GardenGroups.groups(garden)
  |> Task.async_stream(fn group ->
    corners =
      Enum.map(group, fn pos ->
        GardenGroups.count_corners(garden, pos)
      end)
      |> Enum.sum()
    corners * length(group)
  end)
  |> Enum.reduce(0, fn {:ok, cost}, acc -> cost + acc end)
end
puzzle_2.()
Benchmarks
Benchee.run(
  %{
    "puzzle_1" => fn -> puzzle_1.() end,
    "puzzle_2" => fn -> puzzle_2.() end
  })
  
    
      | Name | ips | average | deviation | median | 99th % | 
  
  
    
      | puzzle_1 | 0.23 | 4.36 s | ±0.91% | 4.36 s | 4.39 s | 
    
      | puzzle_2 | 0.22 | 4.52 s | ±4.85% | 4.52 s | 4.67 s |