13
votes
Day 11: Dumbo Octopus
Today's problem description: https://adventofcode.com/2021/day/11
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>
Some definite overlap with day 9, so I was able to reuse a bit of my code. Definitely a fun little simulation. And this time I managed to keep my X and Y coordinates straight. I always enjoy modeling problems
Day 11 Part A - Python
Built a grid and just iterated through it. During the first pass I looked at which ones were going to flash and stored that in a list. Then as flashes lit up their neighbors those got checked to see if they would flash too. Once something did flash I kept track of it to stop it from flashing again and also so I knew to reset it
Day 11 Part B - Python
It's a 10x10 grid, so 100 jellyfish. I just changed the loop to go from counting steps to seeing if 100 jellyfish flashed this iteration
Tips
Building a visualizer may be really helpful so you can see what it was doing
When debugging my simulation, I ran my output and the examples through https://www.diffchecker.com/diff, which is a neat way to tell the difference between blocks of text. It didn't highlight the individual differences, but it could tell me if my grids matched what they were supposed to be. This only works if you manage not to fuck up your x and y coordinates
Like I mentioned above, you may be able to reuse code from previous days, like day 9
The fact that each jellyfish can flash exactly once per step and all if them that flash reset to 0 greatly simplifies aspects of this model
Yay for reusable functions! Just had to add diagonals to my findNeighbors function from day9, and it was ready to be re-used! On a side note, part 2's output was extremely visually satisfying to me. :)
Oh! And I'm extremely proud of my solve time between the two parts. Part 2 only took +3m from part 1, because I was already returning the count of flashes from each iteration, and the comparison for that is a calculable constant!
Part 1
Part 2
Man, reading comprehension really bit me this time.
The actual logic wasn't bad at all, just understanding the rules correctly gave me way more trouble than it should've.
Part 1 Ruby
The important thing that I kept missing is that each octopus only resets its value to 0 /at the very end of each full cycle/, NOT right after updating all its neighbors. You have to keep track of all the octopodes that flashed this cycle (and may now have counters over 10), and reset all of them at once.
Part 2 Ruby
Having finally got my head around the flash rules, the logic for part 2 pretty much exactly the same as part 1, we just keep going until all the values in the grid are the same. I also went ahead and updated my
Grid
class to add some more useful helpers.Updated Grid Class
Day 11 (Rust)
This was another relatively simple day. I liked how you can reuse the parsing code from Day 9, though I forgot to change the input file, so I was simulating a huge octopus cavern at first...
Imports & Data structures
I opted for a kind-of object-oriented style; the Grid struct is generic, while its specialization, OctopusGrid, is responsible for actually keeping track of the octopi energy levels. While
Grid
is kind-of trivial, I'm sure I'll find a way to reuse it in later puzzles.I'm slightly unhappy that the OctopusGrid does not specify the neighbor-relationship; on the other hand, a separate neighborhood function can be reused elsewhere, so maybe that's preferrable.
Parsing (conveniently re-used from day 9)
I wrapped this inside a module this time. Not sure if I like that, but it does seem cleaner and potentially more reuseable.
Helper functions
Something I learned today was that you can wrap values with
Some
in more places than I expected (e.g. in comparisons, not just inif let
statements).Solving
Edit: I love how deep these puzzles are. Even when I think I have a reasonably nice solution, it turns out that it can almost always be much improved if I stare at it for long enough...
Spoilers
For example, I used 255 as the sentinel value for an octopus that has already flashed. However, 0 is a much better sentinel value; we're incrementing all energy levels each step, so 0 does not occur naturally and I can completely eliminate the loop that resets all octopi to zero...
Rust
Surprised to have such a chill day on a weekend.
Rust
Elixir, with concurrency
I think I made this much more difficult for myself, but I learned a lot. I decided to go all in on concurrency and an actor model approach as a learning exercise—I set up agents for each octopus and had them passing messages to one another to simulate the step-by-step level increment and flashes. I also set up a "singleton" counter agent for them to report their flashes to.
The hardest part to code was synchronizing the parts of each step, since during flash propagation each agent kind of just does its own thing without knowing whether it will receive another "flash" message from a neighbor later on. I ended up accomplishing that with a function (
block_until_step_is_done/1
), called from the top-level solution process, that repeatedly asks each octopus agent whether it has any messages left in its process mailbox. Probably not the best approach? But it was the best I could come up with.I also had to explicitly separate the sub-step of the initial level increment from the cascading increments resulting from neighboring flashes. During each step, a message is broadcast to all octopus agents telling them to increment their level, and only once that message has been sent, they get another message telling them to begin flashing.
Runtime: On a 4-core machine, part 1 runs in about 185ms and part 2 in 750ms; with 6 cores, 85ms and 320ms. I'd be really curious how this compares to others' solutions!
FlashCounter
agentOctopus
agentSolution, both parts
Nice and chill, it's neat seeing the evolution of the data through each step.
Part 1
Part 2
I'm sure there's a tiled image or some message in there somewhere
Data Structure
Another hash table from co-ordinate to energy level, but this one is mutable. We'll see why later.
Part 1
A pair of mutually recursive procedures - increase energy level and flash when the energy is over 9
000.Making the hash table and the set of flashed octopodes mutable makes me feel dirty, but it means I can just pass the same data structures between those mutually recursive procedures to accumulate the results.
Part 2
I was expecting this to be one of those never-ending loops where you have to print out partial result and work out some meta pattern to calculate an answer that would otherwise take thousands of years to complete. I do like those problems but this one is not like that.
Python
Add. I had a funny problem. I wrote
octopuses[x][y]
insteadoctopuses[y][x]
because(x, y)
is everywhere, so it took some time to find it out.Rust
Not too bad, but I could've written this cleaner.
Structs
Solution
Just going to do both parts in one today since I'm falling a bit behind, but busy weekends happen when preparing for the holidays. It didn't help that I decided to use this to try and learn how to use Nim's fancy, safe, and garbage collected pointers/
ref
s. That way I could keepref
s to the neighbors and not have to look them up and do loops or anything. Unfortunately, my mental model of how it should work coming from C/C++ needed to be overcome a bit, and then missing the crucialnew
call in mynewOctopus
function meant I was either not really making references and therefore not updating neighbors or managing to have segfaults despite the safety features ofref
. I even spent a lot of time thinking I didn't understand the logic when really I just didn't understand my language!Nim