Advent of Code 2024 - Day 21Keypad Conundrum
| Problem statement | Source code | Tags: PuzzleMemoization
This problem pays tribute to 2019 day 25.
Yeah it's hard. Yeah it may not even be comprehensible. Let's understand what's going on.
Let's call the directional keypad you type 0 and each directional keypad next as 1 and 2 (or 3, 4, etc. for part 2). Finally the numpad is 3 (or 26 for part 2). A sequence of keys on 0 can be translated to a sequence of keys on each successive keypad, and vice versa:
For each keypad, note that its sequence of keys can be seen as a list of segments, each one beginning with A and ending with A (including the numpad one; you can prepend an A to each sequence to see this more clearly, since robots start at the A position anyway). Each A...A segment corresponds to a single key on the next keypad—more precisely, a single key, plus an updated robot position. Conversely, given a single key to type out on keypad n and robot n's current position, we can find the corresponding A...A segment on keypad n-1. This means that when typing a single key on keypad n, robot n's position only affects the key sequence for robot n-1, but no robots before that, because robot n-1's key sequence always starts and ends with A, so it resets its position.
This means that our approach is that given a sequence A...A on keypad n, we can find the corresponding optimal A...A sequence on keypad n-1, all the way until 0. The question is how we should do the mapping. Let's consider a movement from key X to key Y on n.
-
If X and Y are on the same row or column, we should always move in a straight line, because any detour would just involve net extra key presses on
n-1, which doesn't really lead to a better path on any previous keypads. -
If X and Y are on different rows and columns, say Y is to the upper right of X, then we can either move up then right, or right then up. However, we should always move in one direction before changing to the other, because if you alternate between
>and^, then on then-2-th keypad, it would take extra presses to move then-1robot back and forth between the two buttons, as opposed to just pressingArepeatedly onn-2. -
Rule 2 doesn't tell us whether we should move up then right, or right then up. We don't always have a choice. For example, on the directional pad, when moving from
Ato<, we have to go down then left, because the other way would move into the gap. -
If rule 3 still doesn't give an unambiguous path, we now need to consider the next keypad,
n-3, because the number of presses onn-2is the same. Consider^<vs.<^:So there is a difference on
n-3, and<^is better than^<! The reason is because<^collocates the two expensive<presses together, while^<has to move left in two cycles:A<AandA<vA, each one incurring a full left-left-right-right cycle on then-3-th keypad.Similarly,
<vis better thanv<because it also presses<first:In the same spirit that "keys to the left should be pressed first",
v>is better than>v:Finally,
^>is better than>^:So the conclusion is that the priority of moves is:
<>v>^>>.
Now let's code this up. The move_seq function generates a list of to move dx or dy steps in either the x or y direction (but not both):
Now we implement the above 4 rules. For the third rule, we define our coordinate system such that the gap is always at (-2, 0) for both kinds of keypads (which, incidentally, also puts A at (0, 0) for both keypads).
Now we just need to recursively apply this function to unroll the key sequence from the last keypad to the first. The path will become very long so we can't physically hold it as a string, but we just need the mapping from (from, to, depth) to the length of the key presses on keypad 0. This can be recursively defined as where . Since there are relatively few and combinations but a lot of repeated calls on smaller depths, we can memoize the results in a hash table.
Now implementing a sequence of presses on the numpad just requires calling optimal_total_path_cost with the sequence of coordinates.