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

Advent of Code 2024 - Day 08

2024/12.livemd

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(&amp;(&amp;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