Advent of Code 2025 - Day 4: Printing Department
Day 4 brought us back to grid problems. The task involved identifying paper rolls on a grid that forklifts could access, where accessibility is determined by how many neighboring rolls surround each position.
Part 1: Finding Accessible Rolls
Grids are back, and so is my helper library fred.py. The problem is straightforward: count how many paper rolls (@) have fewer than 4 neighboring rolls in the 8 adjacent positions (including diagonals).
The brute force approach works perfectly here:
|
|
Loop through every position, check all 8 neighbors, count the ones with rolls. If fewer than 4 neighbors have rolls, this position is accessible.
First try!
Part 2: Cascade Removal
Part 2 was easier than expected. Once you remove accessible rolls, new rolls might become accessible. Keep removing until no more rolls can be removed.
The Naive Approach
My first implementation used copy.deepcopy() to create a snapshot of the grid, removed all accessible rolls in one pass, then repeated:
|
|
This works. It gave the right answer, but it took 21,146ms - over 21 seconds. On the example it was fine, but the 135x135 input grid made the repeated full-grid scans painful.
The Optimized Approach
The problem with the naive solution is checking every position on every iteration. Most positions don’t change between passes. Why not track only the positions that matter?
The key insight: use a queue. Add all initially accessible rolls to the queue, then:
- Pop a roll from the queue
- Remove it (increment count)
- Check its neighbors - if removing this roll made any neighbor accessible, add that neighbor to the queue
- Repeat until queue is empty
|
|
The helper function get_values_in_dir() returns all 8 neighbors and the center value:
|
|
Results:
- Optimized: 367ms
- Naive: 21,146ms
That’s a 57x speedup. Instead of scanning the entire 135x135 grid repeatedly, we only check positions that could have changed.
Performance Analysis
For a 135x135 grid, the naive approach does:
- Full grid scan: 135 x 135 = 18,225 checks per iteration
- 43 iterations until convergence, for my input
- Total: ~893,000 checks
The optimized approach:
- One initial scan: 18,225 checks
- Queue processing: only checks positions that become accessible (8,890 total)
- Each removal checks at most 8 neighbors
- Total: ~18,225 + (8,890 x 8) = ~89,000 checks
If the grid were larger, the naive approach would scale quadratically with grid size, while the optimized approach scales with the number of removable rolls.
The Lesson
This is a classic BFS/queue problem disguised as a grid simulation. The naive “keep scanning until nothing changes” approach works but misses the structure of the problem. Once you recognize that accessibility changes propagate locally (only to neighbors of removed rolls), the queue-based solution becomes obvious.
The helper library paid off today. Functions like toGrid(), get_value_in_direction(), and addTuples() made the grid manipulation clean. Building up these utilities over previous years of Advent of Code makes later problems faster to solve.
Eight stars total. Both parts solved on first try, then optimized for fun.