Spoiler alert
This is a full solution for Advent of Code 2025, day 7 puzzles with explanations.
If you want to try to solve it yourself first, head over to https://adventofcode.com/2025/day/7 and give it a go and then come back and compare notes.
You can find the full code at https://github.com/Hamatti/adventofcode-2025/blob/main/src/day_7.py
More grids! But 0H-N0, the teleportation machine is broken. From trash compactor to broken teleport room, we’re having a very unlucky weekend. But fear not, as there’s nothing a few dozen lines of Python can’t fix.
Read input
Our input today is a grid consisting of a starting location S and beam splitters ^.
START = 'S'
SPLITTER = '^'
EMPTY = '.'I’m once again wasting some computer cycles when I parse first and then solve but I do believe it makes the code nicer to deal with and especially in a world of Advent of Code where debuggability is a key factor in making sense of my own solutions, I’m very happy to make that tradeoff.
from utils import read_input, create_grid, Coordinate
def is_not_empty(cell: str) -> bool:
return cell != EMPTYOnce again, we’re using my sparse grid generator functions and use this is_not_empty function as our filtering function.
Part 1
In the first part, we need to figure out how many times a beam, moving downwards from starting position, will split by hitting the splitters.
We start with a helper function to find our starting position.
def find_start_position(grid: dict[Coordinate, str]) -> Coordinate:
"""Find a coordinate in the grid for starting positions"""
for position, value in grid.items():
if value == START:
return position
raise ValueError('No start position found')We then start setting up our solution:
def part_1() -> int:
"""Calculate how many times beams get split
when they start from a START position and move downwards."""
inputs = read_input(7, str)
grid = create_grid(inputs, predicate=is_not_empty)
start = find_start_position(grid)
bottom = len(inputs)
split_count = 0We read in the data, create a grid, find the start location, determine the bottom row index and initialise the split count to zero.
beams = set([start.x])
for row in range(1, bottom+1):
new_beams = set()
for col in beams:
hits_splitter = Coordinate(x=col, y=row) in grid
if hits_splitter:
new_beams.add(col-1)
new_beams.add(col+1)
split_count += 1
else:
new_beams.add(col)
beams = new_beams
return split_countWe keep track of beam columns in a set of x coordinates.
For each row, we go through all the x coordinates where a beam is currently at. If that beam would hit a splitter, we create new beams at its left and right side and increment split count. Otherwise, we let that beam continue its path downwards.
Once we reach the end, we’ve successfully counted all the splits.
Part 2
The second part is a trickier one. Now, instead of knowing just how many times we split, we need to calculate in how many different paths can a beam go down.
I was able to code a correct result but I still don’t quite understand how the amount of paths can be smaller than 2 x splits. In the example, there were 21 splits but only 40 paths and I don’t quite still understand how that is possible when each time a beam hits a splitter, there’s two new paths.
We start the part 2 in a similar way, setting up our state variables
def part_2() -> int:
inputs = read_input(7, str)
grid = create_grid(inputs, predicate=is_not_empty)
start = find_start_position(grid)
beam_columns = defaultdict(int)
beam_columns[start.x] = 1
bottom = len(inputs)This time, instead of counting how many beams there are, we keep track of the columns and how many paths have led to them. To make the code simpler later, we once again use defaultdict that I wrote more about yesterday if you want to learn how they work.
Then, we let the beams fall to the bottom:
for row in range(1, bottom+1):
new_beam_columns = copy(beam_columns)
for col in beam_columns:
hits_splitter = Coordinate(x=col, y=row) in grid
if hits_splitter:
new_beam_columns[col-1] += new_beam_columns[col]
new_beam_columns[col+1] += new_beam_columns[col]
del new_beam_columns[col]
beam_columns = new_beam_columnsOn each row, we copy the existing columns to a new one. For each of those columns, if we hit a split, we create two new beams just like in part 1 but this time, we count in how many ways we ended up there. We then delete the column we came from because that can’t continue past a splitter.
Finally, the result is the total number of paths.
return sum(value for value in beam_columns.values()) I also initially coded a recursive solution that found all the paths but since there are a lot… I mean A LOT… of them, that’s never gonna finish. It did work with the example input though so I believe it was functionally correct, just incredibly slow and inefficient.
There’s an extra benefit to writing these types of explanations. They really require me to understand what the code I wrote is doing and why. That’s why I enjoy writing notes about things I learn on a daily basis: it exposes me to gaps in my knowledge and forces me to think them through until I’m able to explain them. Explaining it helps you understand it.
Even if you never explain the topics you learn about to others, start a habit of explaining them to yourself. Write those explanations down. If there’s something you can’t quite explain, research it more.
Get in touch
If you want to comment on these solutions, I share them in Mastodon where you can look for the post for the right date or you can start a discussion over email with juhamattisantala@gmail.com.
Follow via RSS
I have created a custom RSS feed for these solutions so you can follow along and not miss any of them!