11
votes
Day 15: Rambunctious Recitation
Today's problem description: https://adventofcode.com/2020/day/15
Join the Tildes private leaderboard! You can do that on this page, by entering join code 730956-de85ce0c
.
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>
I got stuck in debugging hell for a bit, but eventually got it to work. My solution to part B is almost identical to part A, the only parameter I changed is how far it goes.
Tonight's eurobeat selection. It helped me distract myself from the pain of dropping a weight on my foot earlier.
Day 15 Part A – Python
First think I do is preallocate an area to work with. Then I can load the numbers that I already know of in there. I also create a memory dictionary which will be used to track when any given number was last spoken.
The first few passes deal with numbers already loaded in, so they're not a big deal. I just pass over them. But still dump them into memory and keep track of the 'spoken'
For all new numbers I first check if the 'last spoken' number is in my memory. If not I know it's new, so we get a '0'. Otherwise I do the subtraction as described. Then just update my history, memory, and last spoken.
Day 15 Part B – Python
This is the exact same as part A. The only thing I changed is the value loaded into the 'distance' variable. It takes slightly longer (maybe 30s?), but it's still fine. I suspect if I had designed it differently, it would take much longer.
Tips and Commentary
If you're planning to store the whole history, I highly recommend pre-allocating it since you know exactly how many numbers you're going to care about. If you keep resizing your storage area (for example, through python list's append method) you're going to waste a lot of time unnecessarily. I don't think you actually have to store the history beyond the first few though.
You'll need a way to tell how recently a given number was spoken. You can search for it each time, or you can find a way to store that information so that it can be retrieved more quickly. Searching each time will get slower as your history gets bigger
Make sure you're careful at what point you make your checks and comparisons. It's very important, but the correct order might be non-obvious.
Ruby
I was worried that this would be way harder than it turned out to be.
Part 1
Reading the description had me thinking there was going to be some kind of tricky manipulation involved in part 2, so I built out a separate class to keep track of the history for each number. The biggest slow down for actually solving part 1, at least for me, was reading and re-reading the directions several times to make sure I understood the requirements. Something about the writing here kept making me feel like I was missing something.Part 2
Sometimes brute force works well enough! I set this up and left it to run while I started googling and hitting up the OEIS to see if I could find some other reference for this kind of sequence (I'm sure it's out there somewhere, but I haven't found it yet, as of this comment). Before I'd gotten very far, my terminal pinged me with the correct solution. Turns out my brute force search finishes in about 34s, which is fast enough for now.
I might go back and golf down my solution later, since it could be a bit prettier/more idiomatic.
Python
Repo Link
Part 1
I did the first part in the most straight-forward and naive way possible... just did a reverse linear search to find the previous location of a number. Turns out that Python lists do not have a
rindex
method (strings do though!), so I had to make that myself. Once that was in place, the solution was straightforward.Part 2
For the second part, I knew I had to use a cache/history of some sort, but I just couldn't figure out how to order the logic to search for the last value while also not having in the history. My compromise then was to have a bounded buffer (ie. deque) for each number. For each number, I add it to the corresponding deque and if the deque is length
2
, then I have seen it before. With that structure in place I could determine if I had seen a number before in an efficient manner (no longer need to do a reverse linear search).Overall, the code is still slow (takes about 50 seconds on my computer), but it is still better than my naive solution for the first part.
This is the first day I didn't finish before going to bed, and it really shows the importance of sometimes just stepping back and sleeping on a problem. Last night, I was trying to keep going as fast as I could, and my solution kept adding unnecessary complexity because of the way I started. After I restarted this morning, I knocked it out in ~15min.
This was also my first solution that ran in more than 1s, so that's a fun milestone. Part 2 took 6.996 seconds.
Parts 1/2 (just different loop bounds)
This was a nice simple one I thought. Interesting performance note, I originally did not type the dictionary that I use to store the game state, which was fine in part 1, but was noticeable in part 2. By adding that type, runtime was cut from a little over 21 seconds down to 8.5, of which about one eighth was memory allocation. After looking at some of the solutions here, I decided to tweak a little more, and by not saving every value in the history, switching to an immutable type to store the values, and typing down my numbers, I was able to get the time down even further to a ~2.7 seconds. Not the fastest here, but pretty good I think!
Parts 1 and 2
Racket 💜
Shared Game Engine
Explanation of code evolution
I went through 4 stages.I used an immutable hash table - although Racket handles the indirection of the contents to avoid having thousands of copies of the same values, there is still the overhead of managing memory of all the top level hash table objects - 1 per iteration. This got me both answers though, but I was unhappy with the ~28 seconds for Part 2.
I swapped to a single mutable hash table which sped things up by almost 50% - 17s. At this stage I was using a list of (up to) two previous locations per spoken number which is wasteful and a lot of memory pressure.
I traced out on paper the process and realised I only need to store a single value per iteration - not a list of the previous 2! This brought the runtime down to ~7s.
Finally I converted the hash table to a vector (array) which allocates all the memory at once instead of dynamically expanding the hash table. Down to around 1s. Then I also remembered Racket can use fixed size numbers - instead of generic (unlimited value) numbers (since there can no number greater than 30,000,000) and "unsafe" operations which gave a final 10% boost to about 800/900 ms from 28 seconds! I often like to try out various other approaches once the problem is solved and a different data structure worked well today.
Python
Alright, a bit late, but I did it. I spent way too long looking for some math-y trick but, of course there is none (I'm still paranoid, though). The solution turned out to just store the last index for each number, which isn't a big deal, memory-wise and lead to linear run time. It still takes a few seconds for part 2 and I assume you can optimize it further but I can't be bothered.
Part 1+2
Rust
Solution