AoC 2022 D20: Grove Positioning System
| Problem statement | Source code | Tags: Data structures
← Previous Back to AoC Index Next →
Part 1
This kind of reminds me of 2020 day 23, where we also have to turn things around a circular list, which I implemented as a linked list. This problem needs to implement similar operations. In pseudocode it looks like:
There are two tricky parts:
- Numbers may repeat, so if we search by the value of the number, there may be multiple ones. Instead, we assign each number a unique ID, and search by that ID instead.
- We need to modulo by
list.size - 1instead oflist.size, because the number is removed from the list before being reinserted, so the list size is one less than the original. Thinking about it in another way,n - 1and0are operationally equivalent: they both insert at the end of the list, so we avoidn - 1.
Unlike 2020 day 23, the main difference is that the absolute index matters (not just the relative shifts), so a linked list can't work, unless you'd like to jump 5000 times to the target position. Whatever data structure we choose, we just need to implement three operations: at, find, and move. The trick is to stop thinking about pos as a physical position, but instead as an order. Then:
at(pos): returns thepos-th smallest element (by some key) in the list.find(num): returns the order of the element with the given ID.move(pos, newPos): removes thepos-th smallest element and reinserts it with a new key that makes it thenewPos-th smallest element.
at and find strongly suggest an order statistic tree. The trick for implementing move is to assign the new key as some number between the keys at newPos - 1 and newPos. This way, the element will be in the middle of them without having to update the keys of all the other elements.
You know you are treading deep waters when there's basically no package to do this. I found order-statistic-tree, but it's missing rank. I decided to just copy the code over. Thanks Shlok Datye!
From the pseudocode above, we need each entry in the list to have two things: the number, and an ordering key. The OSTree type expects the entries to be orderable, so we implement Ord by just comparing the keys.
(By the way, I started by defining Key as Ratio Int, but it turned out that the keys can get very large and caused some overflow issues.)
To do anything with tree entries, we need to know the key of the target number. Since I already mentioned that the numbers can repeat, all lookup data structures must be keyed by a unique ID, not the number itself. So we assign each number a unique ID, and store two mappings: from ID to key and to number. The IDs are just the indices of the input list. With these mappings, we just need to iterate over the IDs, mixing each corresponding number.
mix itself is just the implementation of the pseudocode above. Given one ID, it finds the corresponding key and number, calculates the new key, and updates the tree and the mapping.
Finally, to get the 1000th, 2000th, and 3000th numbers after the zero, we just need to find the key of the zero entry (already done in the solve function), and then select the entries at the corresponding ranks.
Part 2
Multiplication of the numbers don't change the algorithm at all (it's used to shoot down solutions that physically move the element num times). Repeating the mixing 10 times just requires replicating the IDs 10 times when iterating over them.