Solution Explanation to Day 22 of Advent of Code 2019
The description of the problem can be found on the Advent of Code website. I usually post my solutions on the subreddit but this time it’ll use so much math notation I think it’ll be more readable here since equations can be rendered with MathJax.
Part 1 doesn’t require any smart tricks or math. Part 2 on the other hand…
I’ll firstly show a less naive approach for part 1 which can then be slightly modified to get us an answer for part 2. Let
- be the number of cards,
- the number of repetitions,
- the number of instructions.
We’ll consider functions , where means that the card at position after applying to the deck will be at position . For factory ordered deck it means is the card.
Notice that all operations can be described in modulo arithmetic:
- Reverse:
- Cut:
- Increments:
Hence, we’ll be working in a field of integers modulo . It sounds scary but what it practically means is that we do each operation modulo, so, for example, We need to be careful though:
- Make sure your language/your functions handle negative numbers correctly. For example, in C/C++
%
is not a modulo operator for negative integers. - Division requires two steps: – find the inverse of modulo and then multiply it and take modulo. (Note: inverse can only be found if , but the numbers in the puzzle are prime so we can inverse any number other than ). How to calculate the inverse? I’ll let you search for that one.
- The numbers are big, multiplication is the most probable to overflow, so a big numbers library or a language which has
BigInt
s by default is recommended. - You’ll have to calculate some big powers but it can be done efficiently with exponentiation by squaring.
Let be the instructions given in the input in the order they appear (for each , or ). Then part 1 can be described as the composition of all these functions:
If you’re not familiar with the notation it means that the final position of card is
Notice that all the functions are affine, so their composition is also affine, therefore
for some .
We can find with a program by using the formulas above. SymPy, Mathematica, or other CAS can calculate it easily. In an imperative language like Python or Julia, one way is to store as an array or a tuple (starting with ) and apply to it each instruction based on the formulas above. So:
- Reverse:
- Cut:
- Increments:
We need to apply times. In other words, we want to calculate . For example:
We can see the inductive pattern, so the formula for an arbitrary is
We can simplify it even further by noticing that the coefficient of is a geometric sum which has closed formula (amazingly, it works for any field, so the formula works with modulo operations too).
Hence, the final formula is
Note: if you want your solution to be more general you should handle the case when separately since there’s no inverse of .
The final algorithm for part 1 is thus: find by applying the given instructions in order, then use formula . Remember to do all operations modulo .
For part 2, we ask the opposite: the position is now the input and the card is the output. This means the problem now asks us for the inverse instead of . From the properties of function composition, this is
So only the first part (before the formula) changes in our algorithm: we apply the inverse of each instruction in reverse order. The inverses of the instructions can be found with simple algebra: solve the equation for and you’ll get the formula for .
Here’s my Julia code.