Powered by AppSignal & Oban Pro

Advent of code 2025 day 7

aoc2025day7.livemd

Advent of code 2025 day 7

Mix.install([
  {:kino, "~> 0.18"}
])

Part 1

https://adventofcode.com/2025/day/7

input = Kino.Input.textarea("Please give me input:")
lines =
  Kino.Input.read(input)
  |> String.split("\n")
  |> Enum.map(&to_charlist/1)

[first_line | other_lines] = lines
start_pos = first_line |> Enum.find_index(fn x -> x == ?S end)
beams = [start_pos]
grid_width = length(first_line)

# It's not defined what happens if two splitters are adjacent,
# but they do not occur in the input.
defmodule Part1 do
  def splitter_positions(line) do
    Enum.with_index(line, fn element, index -> {index, element} end)
    |> Enum.filter(fn {_pos, char} -> char == ?^ end)
    |> Enum.into([], fn {pos, _char} -> pos end)
  end

  def new_beams(line, current_split_count, current_beams, grid_width, deduplicate) do
    splitters = splitter_positions(line) |> MapSet.new()

    {beams_split_count, reverse_dup_beams} =
      Enum.reduce(current_beams, {0, []}, fn pos, {split_count, acc_beams} ->
        if MapSet.member?(splitters, pos) do
          # at the splitter position itself the beam disappears
          # left or right a splitter a new beam appears

          # check left and right border to be sure
          newest_beams =
            if pos > 0 do
              if pos + 1 < grid_width do
                [pos + 1, pos - 1 | acc_beams]
              else
                [pos - 1 | acc_beams]
              end
            else
              [pos + 1 | acc_beams]
            end

          {split_count + 1, newest_beams}
        else
          # no obstructions, beam continues
          {split_count, [pos | acc_beams]}
        end
      end)

    new_beams =
      if deduplicate do
        Enum.dedup(reverse_dup_beams)
      else
        reverse_dup_beams
      end
      |> Enum.reverse()

    {current_split_count + beams_split_count, new_beams}
  end

  def enter_manifold(lines, beams, grid_width, deduplicate) do
    Enum.reduce(lines, {0, beams}, fn line, {split_count, current_beams} ->
      new_beams(line, split_count, current_beams, grid_width, deduplicate)
    end)
  end
end

{total_split_count, _exit_beams} = Part1.enter_manifold(other_lines, beams, grid_width, true)

IO.inspect(total_split_count, label: "Answer part 1, number of splits   ")

# This takes forever:
# {_total_split_count, exit_beams} = Part1.enter_manifold(other_lines, beams, grid_width, false)
# IO.inspect length(exit_beams)

Part 2

defmodule Part2 do
  def splitter_positions(line) do
    Enum.with_index(line, fn element, index -> {index, element} end)
    |> Enum.filter(fn {_pos, char} -> char == ?^ end)
    |> Enum.into([], fn {pos, _char} -> pos end)
  end

  def dedup_with_sum_counts(list_with_pos_and_count) do
    [{_, _} | deduped] =
      Enum.reduce(list_with_pos_and_count, [{-1, 0}], fn {pos, count},
                                                         [{prev_pos, prev_count} | rest_acc] ->
        if pos == prev_pos do
          [{pos, count + prev_count} | rest_acc]
        else
          [{pos, count}, {prev_pos, prev_count} | rest_acc]
        end
      end)
      |> Enum.reverse()

    deduped
  end

  def new_beams(line, current_split_count, current_beams, grid_width) do
    splitters = splitter_positions(line) |> MapSet.new()

    {beams_split_count, reverse_dup_beams} =
      Enum.reduce(current_beams, {0, []}, fn {pos, count}, {split_count, acc_beams} ->
        if MapSet.member?(splitters, pos) do
          # at the splitter position itself the beam disappears
          # left or right a splitter a new beam appears

          # check left and right border to be sure
          newest_beams =
            if pos > 0 do
              if pos + 1 < grid_width do
                [{pos + 1, count}, {pos - 1, count} | acc_beams]
              else
                [{pos - 1, count} | acc_beams]
              end
            else
              [{pos + 1, count} | acc_beams]
            end

          {split_count + 1, newest_beams}
        else
          # no obstructions, beam continues
          {split_count, [{pos, count} | acc_beams]}
        end
      end)

    new_beams =
      dedup_with_sum_counts(reverse_dup_beams)
      |> Enum.reverse()

    {current_split_count + beams_split_count, new_beams}
  end

  def enter_manifold(lines, beams, grid_width) do
    Enum.reduce(lines, {0, beams}, fn line, {split_count, current_beams} ->
      new_beams(line, split_count, current_beams, grid_width)
    end)
  end
end

beams_with_counts = Enum.into(beams, %{}, fn pos -> {pos, 1} end)

{total_split_count, exit_beams} =
  Part2.enter_manifold(other_lines, beams_with_counts, grid_width)

number_of_timelines = Enum.sum_by(exit_beams, fn {_pos, count} -> count end)

IO.inspect(total_split_count, label: "Answer part 1, number of splits   ")
IO.inspect(number_of_timelines, label: "Answer part 2, number of timelines")