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.

First Post!

lkuty

lkuty

#!/usr/bin/env elixir

# Advent of Code 2025. Day 7

defmodule M do
  def go_downward_1(_map, row, _cols, max_row, n_split) when row > max_row, do: n_split
  def go_downward_1(map, row, cols, max_row, n_split) do
    {cols, n_split} = Enum.reduce(cols, {cols, n_split}, fn col, {cols, n_split} = acc ->
      case Map.get(map, {row+1, col}) do
        nil  -> acc
        true -> {cols |> MapSet.delete(col) |> MapSet.put(col-1) |> MapSet.put(col+1), n_split+1}
      end
    end)
    go_downward_1(map, row+1, cols, max_row, n_split)
  end

  def go_downward_2(_map, row, cols, max_row) when row > max_row, do: cols
  def go_downward_2(map, row, cols, max_row) do
    cols = Enum.reduce(cols, cols, fn {col, n}, cols ->
      case Map.get(map, {row+1, col}) do
        nil  -> cols
        true -> cols |> Map.delete(col) |> Map.update(col-1, n, & &1+n) |> Map.update(col+1, n, & &1+n)
      end
    end)
    go_downward_2(map, row+1, cols, max_row)
  end
end

# Setup
{map, {start_row, start_col}, max_row} = File.read!("../day07.txt")
  |> String.split("\n")
  |> Enum.with_index(1)
  |> Enum.reduce({%{}, nil, 0}, fn {line, row}, {map, start, max_row} ->
    line
    |> String.codepoints()
    |> Enum.with_index(1)
    |> Enum.reduce({map, start, max_row}, fn
      {"S", col}, {map, nil, max_row} -> {map, {row,col}, (if row>max_row, do: row, else: max_row)}
      {"^", col}, {map, start, max_row} -> {Map.put(map, {row,col}, true), start, (if row>max_row, do: row, else: max_row)}
      {".", _col}, {map, start, max_row} -> {map, start, (if row>max_row, do: row, else: max_row)}
    end)
  end)

# Part 1
n_split = M.go_downward_1(map, start_row, MapSet.new([start_col]), max_row, 0)
IO.puts "Day 7. Part 1: #{n_split}"

# Part 2
cols = M.go_downward_2(map, start_row, %{start_col => 1}, max_row)
IO.puts "Day 7. Part 2: #{Enum.reduce(cols, 0, fn {_col,n}, total -> total+n end)}"

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

Last Post!

brownerd

brownerd

soooo, nice!

Where Next?

Popular in Challenges Top

bjorng
Note: This topic is to talk about Day 5 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
Aetherus
Finished Day 1 with Elixir :tada: Here’s my code: #!/usr/bin/env elixir defmodule Combination do @doc "Yields each combination of 2...
New
New
Aetherus
Hello, guys. I’m back again, but only for the weekends, maybe. This topic is about Day 13 of the Advent of Code 2020 . Thanks to @egze,...
New
LostKobrakai
This one has been quite the ride. Struggled at first to find a good data format to suite the problem. I really like how that turned out b...
New
bjorng
Here is my solution for day 4: https://github.com/bjorng/advent-of-code/blob/main/2024/day04/lib/day04.ex
New
New

Other popular topics Top

vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40165 209
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
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