kennysliding

December 7, 2025 Transmission_ID: aoc-2025

Advent of Code 2025: Day 7

rust advent of code ai

Problem

Read it here

tl;dr

You have a tachyon manifold (grid) with:

  • S = starting point (beam enters here, moves downward)
  • . = empty space (beam passes through)
  • ^ = splitter (beam stops, emits two new beams going left and right)

Beams continue downward until they hit a splitter or exit the grid. When two beams merge into the same space, they continue as one beam.

Part 1: Count how many times a beam is split (i.e., how many ^ splitters are activated).

Example:

.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

The beam from S goes down, hits the first ^, splits into two beams going left-down and right-down. Those beams hit more splitters, and so on.

Part 1 answer: 21 splits

Key insight: Simulate beams row by row. Track active beam positions. When a beam hits a ^, count it as a split and spawn beams at col-1 and col+1. Beams at the same position merge (use a Set to deduplicate).


Part 2: Quantum twist — it’s a single particle that takes both paths at each splitter, creating timeline splits. Each split doubles the number of timelines. Count total timelines at the end.

The difference:

  • Part 1: Beams merge when at the same position
  • Part 2: Paths don’t merge — each is a separate timeline

Examples of timelines:

  • Always go left at every splitter → 1 timeline
  • Always go right at every splitter → 1 timeline
  • Alternate left/right → 1 timeline
  • etc.

Part 2 answer: 40 timelines

Key insight: This is a dynamic programming problem. Track how many timelines lead to each column position at each row. The state at row N depends only on row N-1. When a particle hits a splitter, each timeline spawns two new ones (left and right). Sum up all timelines at the end.


My Solution (Handcrafted)

Part 1

Straightforward simulation. Use a HashSet to track beam positions, since beams at the same position merge:

pub fn solve_1(filename: &str) -> u32 {
    let input = Self::read_input(filename);
    let s_index = input[0].chars().position(|c| c == 'S').unwrap();

    let mut beam_positions = HashSet::new();
    beam_positions.insert(s_index);
    let mut split_count = 0;

    for row in input.iter().skip(2).step_by(2) {
        let mut new_beam_positions = HashSet::new();

        for beam_position in beam_positions {
            match row.chars().nth(beam_position).unwrap() {
                '^' => {
                    new_beam_positions.insert(beam_position - 1);
                    new_beam_positions.insert(beam_position + 1);
                    split_count += 1;
                }
                '.' => new_beam_positions.insert(beam_position),
                _ => panic!("Invalid character"),
            };
        }
        beam_positions = new_beam_positions;
    }
    split_count
}

I noticed the input alternates between splitter rows and empty rows, so step_by(2) skips the empty ones.

Part 2

This is a dynamic programming problem. Instead of a HashSet, use a HashMap<usize, u64> to track how many timelines lead to each position at each row:

pub fn solve_2(filename: &str) -> u64 {
    let input = Self::read_input(filename);
    let s_index = input[0].chars().position(|c| c == 'S').unwrap();

    let mut beam_path_count: HashMap<usize, u64> = HashMap::new();
    beam_path_count.insert(s_index, 1);

    for row in input.iter().skip(2).step_by(2) {
        let mut new_beam_path_count = HashMap::new();

        for (beam_position, path_count) in beam_path_count {
            match row.chars().nth(beam_position).unwrap() {
                '^' => {
                    *new_beam_path_count.entry(beam_position - 1).or_insert(0) += path_count;
                    *new_beam_path_count.entry(beam_position + 1).or_insert(0) += path_count;
                }
                '.' => {
                    *new_beam_path_count.entry(beam_position).or_insert(0) += path_count;
                }
                _ => panic!("Invalid character"),
            }
        }
        beam_path_count = new_beam_path_count;
    }
    beam_path_count.values().sum()
}

The Bug I Hit

I originally wrote:

*new_beam_path_count.entry(beam_position - 1).or_insert(path_count) += 1;

I thought this would “insert path_count and add 1”. Wrong! It inserts path_count, then adds 1 — giving me path_count + 1 instead of just path_count.

The fix: *map.entry(key).or_insert(0) += path_count — default to 0, then add the count.


Gemini’s Solution

Part 1

Same approach with HashSet, but processes every row and includes bounds checking:

for r in start_row..rows {
    for &c in &current_beams {
        match grid[r][c] {
            '^' => {
                split_count += 1;
                if c > 0 { next_beams.insert(c - 1); }
                if c + 1 < cols { next_beams.insert(c + 1); }
            }
            _ => { next_beams.insert(c); }
        }
    }
    current_beams = next_beams;
}

Part 2

Same DP approach, but Gemini uses Vec<u64> instead of HashMap — cleaner since column indices are contiguous:

let mut current_counts = vec![0u64; cols];
current_counts[start_col] = 1;

for r in start_row..rows {
    let mut next_counts = vec![0u64; cols];

    for c in 0..cols {
        let count = current_counts[c];
        if count == 0 { continue; }

        match grid[r][c] {
            '^' => {
                if c > 0 { next_counts[c - 1] += count; }
                if c + 1 < cols { next_counts[c + 1] += count; }
            }
            _ => { next_counts[c] += count; }
        }
    }
    current_counts = next_counts;
}

No Entry API needed — just vec[col] += count.


Comparison

AspectMy SolutionGemini’s Solution
Part 1HashSet + step_by(2)HashSet, every row
Part 2HashMap + Entry APIVec indexed by column
Bounds checkNoYes

Reflection

Part 2 is a classic dynamic programming problem — compute state row by row, where each row depends only on the previous one. The state is “count of timelines at each column position.”

Gemini’s Vec<u64> is smarter than my HashMap<usize, u64>. Column positions are contiguous integers — perfect for array indexing. No Entry API complexity, just vec[col] += count.

The Entry API bug cost me time. Lesson: *map.entry(key).or_insert(0) += value is the pattern.

Time spent: ~35 minutes