bjorng

bjorng

Erlang Core Team

Advent of Code 2025 - Day 7

Part 1 took much more time than part 2. I started out by reusing my grid parsing function from day 4 and start coding before I had fully understood the splitting rules and how to count splits. It ended up in a mess.

After having finished part 2, I rewrote part 1 using the new parsing routine that I wrote for part 2.

defmodule Day07 do
  def part1(input) do
    {start, splitters} = parse_splitters(input)
    beams = [start]
    split_beams(beams, splitters, 0)
  end

  defp split_beams(_beams, [], num_splits), do: num_splits
  defp split_beams(beams, [splits | splitters], num_splits) do
    {beams, num_splits} = split_beams(beams, splits, [], num_splits)
    split_beams(beams, splitters, num_splits)
  end

  defp split_beams([], _splitters, new, num_splits) do
    {Enum.uniq(new), num_splits}
  end
  defp split_beams([column | beams], splits, new, num_splits) do
    case Enum.member?(splits, column) do
      true ->
        new = [column - 1, column + 1 | new]
        split_beams(beams, splits, new, num_splits + 1)
      false ->
        new = [column | new]
        split_beams(beams, splits, new, num_splits)
    end
  end

  def part2(input) do
    {start, splitters} = parse_splitters(input)
    {timelines, _} = count_timelines(start, splitters, %{})
    timelines
  end

  defp count_timelines(column, splitters, worlds) do
    key = {column, length(splitters)}
    case worlds do
      %{^key => timelines} ->
        {timelines, worlds}
      %{} ->
        {timelines, worlds} = count_timelines_split(column, splitters, worlds)
        {timelines, Map.put(worlds, key, timelines)}
    end
  end

  defp count_timelines_split(_column, [], worlds) do
    {1, worlds}
  end
  defp count_timelines_split(column, [splits | splitters], worlds) do
    case Enum.member?(splits, column) do
      true ->
        {timelines1, worlds} = count_timelines(column - 1, splitters, worlds)
        {timelines2, worlds} = count_timelines(column + 1, splitters, worlds)
        timelines = timelines1 + timelines2
        {timelines, worlds}
      false ->
        count_timelines(column, splitters, worlds)
    end
  end

  defp parse_splitters(input) do
    splitters = input
    |> Enum.map(fn line ->
      String.to_charlist(line)
      |> Enum.with_index
      |> Enum.flat_map(fn {char, col} ->
        case char do
          ?. -> []
          ?S -> [{:start, col}]
          ?^ -> [col]
        end
      end)
    end)
    [[{:start, start}] | splitters] = splitters
    {start, splitters}
  end
end

EDIT: Further simplified part 1 by removing vestiges of my messy first solution.

Most Liked

hauleth

hauleth

Parse

[start | rest] = String.split(puzzle_input)

start_col = byte_size(start) - byte_size(String.trim_leading(start, "."))

splitters =
  Enum.map(rest, fn row ->
    row
    |> String.to_charlist()
    |> Enum.with_index()
    |> Enum.filter(&(elem(&1, 0) == ?^))
    |> MapSet.new(&elem(&1, 1))
  end)

Part 1

Enum.reduce(splitters, {MapSet.new([start_col]), 0}, fn splits, {beams, count} ->
  import MapSet, only: [intersection: 2, difference: 2, union: 2]
  
  hits = intersection(beams, splits)
  new_beams = for hit <- hits, dx <- [-1, 1], into: MapSet.new(), do: hit + dx
  beams = beams |> difference(hits) |> union(new_beams)

  {beams, MapSet.size(hits) + count}
end)

Part 2

Enum.reduce(splitters, %{start_col => 1}, fn splits, beams ->
  Enum.reduce(splits, beams, fn s, acc ->
    case Map.pop(acc, s) do
      {nil, map} ->
        map

      {count, map} ->
        Map.merge(
          map,
          %{
            (s + 1) => count,
            (s - 1) => count
          },
          fn _k, a, b -> a + b end
        )
    end
  end)
end)
|> Enum.sum_by(&elem(&1, 1))

EDIT

I decided to draw the resulting image

rvnash

rvnash

I think it’s called “Pascal’s Triangle”, dunno maybe I’m wrong. Anyway, I naively did Part 2 and of course it doesn’t complete in the age of the universe. So, decided to keep track of both timelines and tachyons in one pass. Went much better. Used MapSet and Map to speed lookups. Fun day!

Edit: It is Pascal’s Triangle except with the twist that the tree doesn’t split necessarily evenly on each level.

defmodule RAoc.Solutions.Y25.Day07 do
  alias AoC.Input

  def parse(input, _part) do
    Input.read!(input)
    |> String.split("\n", trim: true)
    |> then(fn [first_line | rest] ->
      {first_line
       |> String.graphemes()
       |> Enum.with_index()
       |> Enum.filter(fn {c, _n} -> c == "S" end)
       |> List.first()
       |> elem(1),
       rest
       |> Enum.map(fn line ->
         line
         |> String.graphemes()
         |> Enum.with_index()
         |> Enum.reject(&(elem(&1, 0) == "."))
         |> Enum.map(&elem(&1, 1))
         |> MapSet.new()
       end)}
    end)
  end

  def part_one(problem) do
    tach_counters(problem)
    |> elem(1)
  end

  def part_two(problem) do
    tach_counters(problem)
    |> elem(0)
    |> Enum.sum_by(fn {_, n} -> n end)
  end

  defp tach_counters({tachyon, rows}) do
    tachyons_w_timelines = %{tachyon => 1}

    Enum.reduce(rows, {tachyons_w_timelines, 0}, fn row, {tachyons_w_timelines, split_count} ->
      Enum.reduce(tachyons_w_timelines, {tachyons_w_timelines, split_count}, fn {n, last},
                                                                                {tachyons_w_timelines,
                                                                                 split_count} ->
        if MapSet.member?(row, n) do
          {tachyons_w_timelines
           |> Map.delete(n)
           |> Map.update(n + 1, last, &(&1 + last))
           |> Map.update(n - 1, last, &(&1 + last)), split_count + 1}
        else
          {tachyons_w_timelines, split_count}
        end
      end)
    end)
  end
end
dompdv

dompdv

Simplified after doing Part2 because Part2 solves also Part1.

defmodule AdventOfCode.Solution.Year2025.Day07 do
  def part1(input), do: input |> fire_beam() |> take_counter()
  def part2(input), do: input |> fire_beam() |> take_timelines() |> Map.values() |> Enum.sum()
  def take_counter(a), do: elem(a, 1)
  def take_timelines(a), do: elem(a, 0)

  def fire_beam(input) do
    [[start] | rows] = parse(input)
    beam(Map.new([{start, 1}]), 0, rows)
  end

  def beam(in_flight, hits_counter, []), do: {in_flight, hits_counter}

  def beam(in_flight, hits_counter, [next_row_splitters | rows]) do
    # in_flight %{column => number of timelines leading to this point}
    # hits_counter : number of hits on a splitter
    {new_in_flight, new_hits} =
      Enum.reduce(in_flight, {%{}, 0}, fn {col, n_timelines}, {next_positions, hits} ->
        if col in next_row_splitters,
          do: {
            next_positions
            |> Map.update(col - 1, n_timelines, &(&1 + n_timelines))
            |> Map.update(col + 1, n_timelines, &(&1 + n_timelines)),
            hits + 1
          },
          else: {Map.update(next_positions, col, n_timelines, &(&1 + n_timelines)), hits}
      end)

    beam(new_in_flight, hits_counter + new_hits, rows)
  end

  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.reduce([], fn line, lines ->
      splitter_cols =
        to_charlist(line)
        |> Enum.with_index()
        |> Enum.reduce([], fn
          {?., _}, acc -> acc
          {?^, col}, l -> [col | l]
          {?S, col}, l -> [col | l]
        end)

      if splitter_cols == [], do: lines, else: [splitter_cols | lines]
    end)
    |> Enum.reverse()
  end
end

Where Next?

Popular in Challenges Top

maennchen
Ok, that was a rough one today. I haven’t found a way to improve the algorithm further. Part 1 runs in .5 seconds, Part 2 in ~ 5 minutes...
New
rugyoga
Not the prettiest but it works https://github.com/rugyoga/aoc2023/blob/main/lib/2024/9.ex
New
stevensonmt
Reasonably pleased with my solution. The bitstring packet problems are so well suited to Erlang/Elixir it’s almost not fair. defmodule D...
New
Aetherus
This topic is about Day 7 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
Aetherus
This topic is about Day 15 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/l...
New
Qqwy
Note by the Moderators: This topic is to talk about Day 6 of the Advent of Code. For general discussion about the Advent of Code 2018 an...
New
bjorng
Note: This topic is to talk about Day 6 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
cblavier
Hi, there :wave: Today, I felt it was way more challenging! I went through part2 thanks to Agent based memoization (without memoization ...
New
bjorng
Note: This topic is to talk about Day 23 of the Advent of Code. For general discussion about the Advent of Code 2018 and links to topics...
New
Aetherus
This topic is about Day 16 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/l...
New

Other popular topics Top

New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52341 488
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement