11
votes
Day 6: Guard Gallivant
Today's problem description: https://adventofcode.com/2024/day/6
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>
Oof, I was slow on this one. Part 1 wasn't too bad with my usual "simulation" approach, but Part 2 took three minutes to run. Definitely room for optimization.
Part 1
Part 2
For part 2, you don't need to check every possible location. You only care about places the guard walked during a full go of part 1. That alone got me down from ~30 seconds to ~7.5 seconds in Python!
This takes about 4 seconds on my machine in release mode, and about 15 in debug... so, I feel there has to be a smarter way to solve this. Or maybe my code specifically is just very bad, I don't know.
EDIT: Okay, after reading about people getting better execution times than me in Python, I'm almost sure I did something wrong. Might take another stab at this later today.
EDIT 2: I fixed it! It now takes half a second in debug mode, and about 50 milliseconds in release mode. Much better. It turns out all I had to do was replace the set used for cycle detection with an array, and the program became an order of magnitude faster. I've replaced the code below with the new solution.
Solution (Jai)
Part two was rough.
I had a nice solution, but it didn't work for the full input.
I worked on it for a while, but eventually gave up and did a brute force solution.
While it was running, I realized why my nice solution wouldn't work in all cases.
And the extension to make it work is a lesser brute-force, but I also have to juggle a bunch of extra temporary state.
This is the first one where the performance of Rhai really reared its head (not only is it a tree-walk interpreter, it passes everything by value).
It took 7:48 to run part two.
I'll likely come back later and clean it up with a better solution.
I have a few ideas for what to do better, but I'm a bit tired and salty at the moment.
Rhai Solution
TypeScript
I like how the time-travel plot in today's story sets up a weird unexplained detail in one of the puzzles from six years ago, that was a nice touch! I didn't run into the same brute-force performance issue on Part 2 that others did, explanation below...
Spoilers
Quick note that since every turn is 90° clockwise, I was able to set up a
rotate()
convenience function and just call that whenever the guard hit an obstacle.I hate code duplication so my solution to both parts lives in a single function, and one of the args is just a flag indicating what you want it to return (either the count of distinct guard positions, or the count of valid obstacle positions).
The first thing it does is figure out the starting position and direction, then it replaces that tile in the map with a
.
so it matches all the other non-obstacle tiles. This is important, because in the next phase (the walk) the guard will paint the tile she's on to track where she has been and what direction she was facing the first time she was there. We can use this to validate that a position is distinct before counting it, and also use it to detect infinite loops in Part 2 without wasting cycles on simulating them. The idea being, that if the guard enters a tile already marked with the direction she's currently facing, we know she MUST be in a loop.A few other small optimizations like breaking early out of loops helped a bit too; both parts together complete in 2795ms on my machine. I kind of hate the flag argument pattern I landed on here in my pursuit of DRYness but it's good enough for AoC.
Parts 1 and 2 (TypeScript)
Part 2 was fun. I had a basic plan going into it, but came up with a few more little optimizations along the way.
Some of these optimizations were large time complexity -> space complexity tradeoffs, though, as I was holding onto a lot of history along each step of each re-run of the guard's walk.
The end result runs in 1.8 sec, which seems pretty solid based on what others have reported.
One of the main tricky things was knowing when I needed to be checking just the guard's position vs her position and heading. I had to fix a few bugs related to this. I probably didn't do myself any favors by using nested tuples to structure the data. Maps or even structs would have made it easier to make sense of things.
Since functional programming is all about obstinately refusing to use loops like a normal person (They're procedural!! 😱🤢🧐) I modeled the guard states using
Stream
functions, which can lazily produce and manipulate infinite series.Both parts
I used my existing
Grid
module to parse the input. I might create a companionHeading
module soon as well, because I keep treating headings as a special case of points/coordinates, when they're kind of a whole different thing.Benchmarks
Did the brute-force the same as others; about 35 seconds to run. Not super proud to be getting slow execution times already at day 6, but I wasn't aiming for time this year so we'll push onward.
I figured out how to make a class hashable, which I suspect will be a useful tool, and it seems like Pharo's
clone
is sufficient for copying at least non-nested objects. I'd like to explore more about the debugger tools. Being able to live edit methods is super cool, but I suspect there's a lot more I'm missing.Pharo Smalltalk Solution
Python: Step-by-step explanation | full code
Part 1 I did naively (gotta get those in while I can 😅). For part 2, I went on a bit of a refactoring journey to adapt my part 1 code for loop detection (which meant tracking both location and direction) and then speeding it up by only checking the paths the guard actually walked.
Nothing too complicated, but it made for a nice writeup explaining the journey of refactoring safely and ensuring every intermediate step still passed part 1. Runs in
7.5 seconds on Python
3.12.7`, which I'm pleased enough about to not keep iterating on it.Having the same frustration as basically everyone else here. Part 2 works, but it's slow (roughly 20 seconds for both CPython and PyPy). I would like to think about the problem more and come back to it in a few hours, but future me might be too tired. I did get to use complex numbers again, though, which makes me happy. My Python solutions:
Part 1
Part 2
This one was simple and quick. Why couldn't the previous ones be so nice?
Brute-forcing part 2 runs a bit slow, but eh.
Edit: Did a bit of optimization on part 2, now runs in 0.35 seconds. The speed-up mostly comes from not cloning a HashSet every time the guard takes a step.
Part 1 (Rust)
Part 2 (Rust)
Part 2 took at least 90 minutes to run, I literally went to my work Christmas party and just came back to it being done, so maybe longer xD
Racket
I think most of the time is because I used a list of lists for the map instead of a vector of vectors, so trying to replace a single value is O(ij) instead of O(1). Honestly though, I think my part 1 is decent enough.
I took too much time on this because I tried to optimize and while doing so missed something.
TLDR: My optimization was to just start from the point in the path where I put the obstacle, but that doesn't account for when the obstacle would be counted earlier...