7
votes
Day 4: Printing Department
Today's problem description: https://adventofcode.com/2025/day/4
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Finally, a character grid problem. If you've done one of these before, hopefully you've saved up some utility methods from previous years to solve this one in a snap.
Thoughts
My old utility code already had methods for (1) parsing the input into an array of chars; (2) give you an IEnumerable of all coordinates in the grid; (3) find the coordinates of the eight neighbors of a coordinate; and (4) replace the character at a coordinate. All I had to write is checking if the neighbor count of `@` is less than 4., and write a loop to replace them until we can't anymore. Nothing really challenging here, and the code runs mighty fast without the need for any micro-optimizations.
C# code
Elixir
I've built up a pretty robust
Gridmodule over the years—maybe concerningly robust given that it's used solely for a bunch of toy code puzzles—so today's solution was a quick one.No special optimizations, I just took the obvious approach.
Both parts
Benchmarks
Bonus: Animated passes
I exported an image of the grid after each forklift pass and assembled a looping animation.
Huh, I was scratching my head a bit if there isn't a better / more efficient way to handle arrays in elixir, but also just opted for a map of coordinates. Minus some naming, fancy language features and your grid module, we landed on pretty much the same solution with very similar performance characteristics too.
(each part parses once, so that's included here)
Rust
Part 1, 69 µs - Simple 3x3 kernel sliding over every "pixel". I started doing a GPU implementation, which I've never done before and I was horrified at how much boilerplate code it requires, and that just setting up the GPU pipeline takes ~100ms. I ditched that idea before I even made it past the GPU example code and went back to the CPU...
Part 2, 847 µs - Smallest tweak I've had to make yet for a part 2--I just remove
@s as I go and keep looping until no more@s are removed in a pass. I thought briefly about getting fancy and trying to track cells that had changes, but I think the move here is just to place my trust in the CPU cache and keep the loop doing a simple linear traversal over memory? I'm not certain about that, though.TypeScript
Fairly straightforward today. I did spend some extra time working on optimizations. I'm formatting the grid as
boolean[][]to avoid working with strings, and I found that I could determine roll accessibility by just totaling all neighboring cell values if I cast those bools as numbers (either 0 or 1) usingNumber(grid[row][col]).In Part 2 I was initially detecting stability by comparing hashes of the previous and current grid array in each step. It was a real forehead-smacker when I realized I could just check my count of how many rolls were removed in the current iteration, and if the number's zero that means it's stable. Ripped out a lot of slow code to clean that up!
I was also naively doing a lot of deep clones of the grid array early on, and swapped that out for a 2-grid (current and next) approach that removed a lot of overhead. I'm relatively happy with where this ended up in the end.
Parts 1 and 2
Part 1 completes in 7ms on my machine; Part 2 completes in 56ms. I have to admit I'm jealous of some of you Rust folks, et al., who are measuring sub-millisecond times for these. I think that kind of performance is always going to be out of reach for my stack! I'm not even sure if the capability exists for me to measure with that kind of precision.
Benchmarking is one of those things that looks pretty simple on the surface (just time it, right?) but actually goes decently deep. The general idea is you don't just measure the code once--you run it in a loop many many times so that you can see how long it takes on average. You also want a warmup phase in which you don't measure the first few seconds of looping. JIT-compiled languages need this warmup because they need to optimize the code, but even AoT-compiled languages need it because the computer has a bunch of sneaky caches lying around in the filesystem and the CPU that you want warmed so your timing becomes consistent. You've also got to be careful that the compiler doesn't notice your benchmark setup isn't using the result of the computation, lest it optimize the computation away completely.
It's very likely that most of that time you're measuring is due to the JS interpreter not being warm, so it's super unfair to compare it to the AoT language gang where we don't have that problem... and also I'm using a benchmarking framework that's making sure all those other sneaky caches I mentioned are also warm.
Very good points here. Currently my AoC runner just grabs a start timestamp and an end timestamp and prints the difference. It only has millisecond precision and I do get varying results each time I run it.
Not that this is particularly important but I can probably improve that in a few different ways. Could be a fun exercise. I’ll poke at it tomorrow.
@zkxs Just finished up my benchmarking improvements! Haven't started today's puzzle yet though, lol...
Some of the improvements I made:
And check out my fancy new tabular printout:
It's wild what a difference it makes — the benchmarks I posted here yesterday were 7ms for Part 1 and 56ms for Part 2. Without changing any of my solution code I've more than halved my numbers and have more confidence their accuracy. Feels good.
Over two hours to write and over a minute to run...
Yesterday I would have said Prolog didn't seem so bad and I was getting a feel for things, today I spent the better part of an hour getting a Moore neighborhood to work.
Real tired and pretty disappointed after this one. Hopefully get a second wind with some free time this weekend.
Prolog Solution
I sat there staring at part 1 for a while until I realized I'd made an incredibly obvious mistake, and after fixing that part 1 worked and part 2 was trivial.
Thoughts
The mistake was that I was counting all grid cells that had less than 4 paper roll neighbors, when I actually needed to count only the grid cells that contained a paper roll and had less than 4 neighbors. Here's my solution for part 2, in Python today:
I saw some comments on reddit saying that based on previous years, the difficulty should start ramping up soon, especially with only twelve days this year. For those of you who have done previous years, does it get progressively harder, and what do you think of this year's difficulty so far?
I'm also expecting the difficulty to start ramping up soon, although we might still get 1 or 2 days for which a brute force approach still works.
I only finished all days in 2020 due to time constraints, but from what I remember, the difficulty increases over time with the only exception being the 25th that was always super simple. With the shorter schedule, I would expect the 12th problem to be the hardest since there would be no obvious reasons to break the pattern.
The problem itself wasn't super difficult. Even part 2 could likely be implemented by repeatedly iterating over the whole map, removing what could be removed until nothing could be removed. This approach can be improved by having a list of cells whose neighbors have been modified and need rechecking. Even better, keeping track of the number of neighboring rolls + a list of rolls to be removed allows us to directly know if a neighbor is newly removable when removing a roll.
On the Rust side, today was about learning many of the pitfalls when using closures the same way I sometimes use C++ lambdas with auto arguments. At first, I had tons of issues with the type inference, where
|x, y| data[x][y]doesn't compile, even though|x, y| fn(x, y)does, which I find confusing. Then, I had to satisfy the borrow checker because if I want to mutate a capture inside a closure, I cannot use this object outside of the closure. My initial solution was to explicitly pass the stuff I wanted to mutate as argument to the closure, but I later transitioned to a nicer (maybe?) solution where I put my data into a struct and declared methods for the struct. Hopefully I'll get familiar with all those things before I end up in a situation where I'll need to use explicit lifetime annotations.Part 1 and 2 (Rust) after tons of refactoring and cleanup
Python
Standard brute-force method of solving both of these and didn't take too long on my machine (sub 1s for both) so I'm happy enough to share my solution. I'm used to there being some kind of edge case that my test input didn't capture, but it was pretty straight forward today and I didn't end up running into any of those problems.
I'm also going to remove my utility
grid_atmethod because... well you can imagine how that's implemented and it's just noise in the code (and I did not save any grid utilities from last year...)Part 1
Part 2
Relatively easy problem (easier than yesterday's, I think), though I took some time today to make my solution more efficient. The naive bruteforce solution only took about half a second to run on my machine, so it wasn't critical to improve it, but I could tell there was room for improvement and so wanted to give it a try. Probably my solution here is somewhat overengineered (it also has some repetitive code), but it runs more or less instantly, so I'm happy enough with it.
Solution (Lily)
Set module
This was a fun one! It strikes that perfect balance of hard enough to make you think, but simple enough to have a pleasing solution after ~30 minutes of work.
After writing some utility generators to yield positions with paper rolls and positions adjacent to them, it was an easy solve with Python sets and Counters. I didn't want to deal with conditional logic for what's "inside" the bounds of the grid; so I forewent a list in favor of a sparse set of roll positions and heatmap of adjacent roll counts.
There's some performance left on the table for Part 02 with the way I check all remaining rolls to calculate the next batch of rolls to remove after removing the previous batch. Doing them one at a time and maintaining a stack of rolls would be more efficient. I could check only the remaining roll positions when updating the adjacent counts and push them onto the stack when they fall below 4. But this way made for a cleaner, simpler solution that runs in 0.5s on my old Thinkpad.
Part 1
Part 2
@zkxs I stole your code for populating the grid from a file just because it was cleaner than mine and not particularly relevant for the actual challenge. I don't like defining the 139 size, I would rather read line length from the file from the argument, but it needed to be defined for the recursive function signature. Is this where macros come in?
I learned a long time ago that when dealing with neighboring cells on a grid, it's often easier to just extend the grid by one in every direction and fill it with junk to make all of the neighbor checks cleaner.
I solved this one earlier using the loop method where you keep checking the entire grid for any available to remove and removing them, but I wanted to take a swing at a recursive option. Iterate over the grid once, but whenever you remove a roll, see if it makes any new rolls available to be removed.
This runs in 5ms on my machine which isn't really faster or slower than the original approach, but recursion always tickles my brain in a good way so I prefer this way.
Part 2