Advent of Code 2025: Day 7
Problem
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 ¤t_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
| Aspect | My Solution | Gemini’s Solution |
|---|---|---|
| Part 1 | HashSet + step_by(2) | HashSet, every row |
| Part 2 | HashMap + Entry API | Vec indexed by column |
| Bounds check | No | Yes |
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