Powered by AppSignal & Oban Pro

Advent of code 2025 - Day 7

day07.livemd

Advent of code 2025 - Day 7

Description

Day 7 - Laboratories

defmodule Load do
  def input do
    File.read!("#{__DIR__}/inputs/day07.txt")
  end
end
defmodule Day7 do
  defp parse_grid(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {line, row}, grid ->
      line
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce(grid, fn
        {char, col}, acc when char in ["S", "^"] -> Map.put(acc, {row, col}, char)
        _, acc -> acc
      end)
    end)
  end

  defp find_start(grid) do
    Enum.find_value(grid, fn
      {pos, "S"} -> pos
      _ -> nil
    end)
  end

  defp max_row(grid), do: grid |> Map.keys() |> Enum.map(&elem(&1, 0)) |> Enum.max()

  def part1(input) do
    grid = parse_grid(input)
    start = find_start(grid)
    max = max_row(grid)

    simulate(MapSet.new([start]), grid, max, 0)
  end

  defp simulate(positions, grid, max, splits) do
    {next_positions, next_splits} =
      Enum.reduce(positions, {MapSet.new(), splits}, fn {row, col}, {acc, count} ->
        next_row = row + 1

        cond do
          next_row > max ->
            {acc, count}

          Map.get(grid, {next_row, col}) == "^" ->
            {acc |> MapSet.put({next_row, col - 1}) |> MapSet.put({next_row, col + 1}), count + 1}

          true ->
            {MapSet.put(acc, {next_row, col}), count}
        end
      end)

    case MapSet.size(next_positions) do
      0 -> next_splits
      _ -> simulate(next_positions, grid, max, next_splits)
    end
  end

  def part2(input) do
    grid = parse_grid(input)
    start = find_start(grid)
    max = max_row(grid)

    {count, _memo} = count_paths(start, grid, max, %{})
    count
  end

  defp count_paths({row, col} = pos, grid, max, memo) do
    case Map.fetch(memo, pos) do
      {:ok, cached} ->
        {cached, memo}

      :error ->
        next_row = row + 1

        cond do
          next_row > max ->
            {1, Map.put(memo, pos, 1)}

          Map.get(grid, {next_row, col}) == "^" ->
            {left_count, memo} = count_paths({next_row, col - 1}, grid, max, memo)
            {right_count, memo} = count_paths({next_row, col + 1}, grid, max, memo)
            total = left_count + right_count
            {total, Map.put(memo, pos, total)}

          true ->
            {count, memo} = count_paths({next_row, col}, grid, max, memo)
            {count, Map.put(memo, pos, count)}
        end
    end
  end
end
ExUnit.start(autorun: false)

defmodule Test do
  use ExUnit.Case, async: true

  @input """
  .......S.......
  ...............
  .......^.......
  ...............
  ......^.^......
  ...............
  .....^.^.^.....
  ...............
  ....^.^...^....
  ...............
  ...^.^...^.^...
  ...............
  ..^...^.....^..
  ...............
  .^.^.^.^.^...^.
  ...............
  """

  test "part 1" do
    assert Day7.part1(@input) == 21
  end

  test "part 2" do
    assert Day7.part2(@input) == 40
  end
end

ExUnit.run()
Day7.part1(Load.input())
Day7.part2(Load.input())