Metadata
-
Date
-
Tagged
-
Part of series
- Advent of Code 2024 Day 1
- Advent of Code 2024 Day 2
- Advent of Code 2024 Day 3
- Advent of Code 2024 Day 4
- Advent of Code 2024 Day 5
- Advent of Code 2024 Day 6
- Advent of Code 2024 Day 7
- Advent of Code 2024 Day 8
- Advent of Code 2024 Day 9
- Advent of Code 2024 Day 10
- Advent of Code 2024 Day 11
- Advent of Code 2024 Day 12
- Advent of Code 2024 Day 13
- Advent of Code 2024 Day 14
- Advent of Code 2024 Day 15
- Advent of Code 2024 Day 16
- Advent of Code 2024 Day 17
- Advent of Code 2024 Day 18
- Advent of Code 2024 Day 19
- Advent of Code 2024 Day 20
- Advent of Code 2024 Day 21
- Advent of Code 2024 Day 22
- Advent of Code 2024 Day 23
- Advent of Code 2024 Day 24
- Advent of Code 2024 Day 25
-
Older post
-
Newer post
Advent of Code 2024 Day 21
Day 21: Keypad Conundrum
https://adventofcode.com/2024/day/21
Another day, another familiar location.
There is a door that needs to be opened by entering a bunch of codes on a keypad.
An example input looks like this:
Each line is a sequence of keys that should be pressed.
That A
is the submit key.
A numeric keypad looks like this:
But there is a wrinkle (ofcourse there is),
The codes have to be entered by a robot, robots can be controlled through another keypad, a directional one.
A directional keypad looks like this:
A robot arrives at a keypad with their “finger” pointed at A
.
Each instruction pressed on a directional keypad causes the other robot’s “finger” to move one spot in that direction,
or press the key they are currently pointing at if the instruction was A
.
A robot can never point at the empty space on the keypad they are at, else … they blow up or something? Bad things happen, better not find out.
Parsing
Turning the input into a list of codes.
Part 1
There is an additional wrinkle, the robot controlling the numeric keypad robot has to be controlled by a robot. You control that robot with your own directional keypad.
I heard you like controlling robots, so we made you control robots while you control robots
The problem statement has a detailed explanation and examples.
To summarize:
- One directional keypad that you are using.
- Two directional keypads that robots are using.
- One numeric keypad (on a door) that a robot is using.
The complexity of a single code (like 029A) is equal to the result of multiplying these two values:
- The length of the shortest sequence of button presses you need to type on your directional keypad in order to cause the code to be typed on the numeric keypad; for 029A, this would be 68.
- The numeric part of the code (ignoring leading zeroes); for 029A, this would be 29.
The question asks for the sum of all complexities for the codes in your input.
The solution here is done with a lot of hindsight, my original solve was much messier. But I left in some of the logic duplication in the code for the individual parts because it’s cleaner to explain.
This is really 2 grid problems in disguise.
- A numeric keypad one
- A directional keypad one
During the solving of the numeric keypad one, I solve the directional keypad one. Several times! As there are multiple directional keypads to control.
Some skeleton/pseudocode I will works towards:
Helpers
As this is a grid problem, you might know what’s coming, the holidays Point
!
The Point
struct has a method that returns the new location of a point when you sent it an instruction.
And optionally, an output (if that instruction was an A
).
It takes in a map of the keypad on which the instruction should be executed, those maps are made by my very next helper functions.
Then 2 helpers that turn keypads into maps from Point
to the character at that location.
Again, not the most efficient way to store it, but one I like a lot.
And today, it pays off because of the empty spots that are not part of the grids!
It’s time to start working towards that solve
function from the skeleton.
It returns the length of the sequence you have to enter on your keypad.
But it’s really solving for the shortest path, and what’s being navigated is the numeric keypad. Time for another round of our good pal Edsgser!
If you want a refresher, I used that algorithm in day16, and there are many online resources on it.
First the definition of the node in the priority queue, then the shortest path algorithm itself.
I chose to let the solve
function take in a list of characters instead of the raw string.
The same thing, but a bit easier to index into, which I do.
In that code, I use a calc_cost
function to calculate how much a move for the numeric keypad would cost at the final directional keypad I am controlling.
That’s a recursive function!
It returns the cost for a certain amount of robots (called depth
) in code.
Each time I recurse, I decrement depth
by 1.
The base case for the recursion is when depth
reaches 0, then the cost is 1 (meaning: 1 move has 1 cost).
It also start off as a regular Dijkstra shortest path algorithm, but recurses each time the new cost for a node is calculated.
The node in this priority queue is slightly different to the one in the queue for solve
,
instead of keeping track of the length of the path, it keeps track of the previously output of executing an instruction (can be empty!).
Code
Put them all together, and it’s very close to the original skeleton.
Part 2
Now the number of intermediary robots is 25.
- One directional keypad that you are using.
- 25 directional keypads that robots are using.
- One numeric keypad (on a door) that a robot is using.
The code for part1 is nearly correct.
Changing the hardcoded 2
to a 25
makes is correct.
But it’s too slow.
A trick from previous days: slap a cache on that recursion and bam, we’re dynamically programming over here!
And the solve
function where that cache gets constructed and passed to the original recursive function call.
Code
Same as part 1, the changes were in the helpers.
Final code
- I made the depth to recurse a parameter instead of hardcoding it
- I condensed the two node types into one and stored the specific part in a tuple in each function’s queue.
Making those changes to the code for part2: