Advent of Code 2023-Day 8: Haunted Wasteland
Day 8 involves navigating binary trees, and modular arithmetic, but I'm getting ahead of myself. Here is our test input:
RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)
The first line is a list of directives to choose either the left L or right R child of a pair. The other block of lines give keys and a pair of keys. Let's parse the lines of the real input file as atoms:
'("LLRLLRLRLRRRLLRRRLRRLRLRLRLRLRLRRLRRRLRLLRRLRRLRRRLLRLLRRLLRRRLLLRLRRRLLLLRRRLLRRRLRRLRLLRLRLRRRLRRRLRRLRRLRRLRLLRRRLRRLRRRLLRRRLRLRRLLRRLLRLRLRRLRRLLRLLRRLRLLRRRLLRRRLRRLLRRLRRRLRLRRRLRRLLLRLLRLLRRRLRLRLRLRRLRRRLLLRRRLRRRLRRRLRRLRLRLRLRRRLRRLLRLRRRLRLRLRRLLLRRRR")
'()
'("TFN" "SMC" "LQT")
'("JKL" "XDN" "KPK")
'("JMF" "HGP" "QKF")
I'd like to parse the nodes into a hash with the value being a cons cell. This will allow retrieving the left value with car and the right value with cdr.
(define nodes (for/hash ([ lst (in-list (drop lines 2)) ])
(match-let ([ (list key left right) lst ])
(values key (cons left right)))))
(writeln (hash-ref nodes "TFN"))
("SMC" . "LQT")
Since I'm representing the pairs as a cons cell, I'll parse each L character to car and each R character to cdr, so I can use those directly to retrieve the values from the cons cells:
(define directions (~> (first lines) ; First line is a list of one string
first ; First element of first line i.e. the string
(string->list _) ; Convert to a list of characters
(map (λ (c) ; Map each L to car and each R to cdr
(match c
[ #\L car ]
[ #\R cdr ])) _)))
(writeln (take directions 5))
(#<procedure:car> #<procedure:car> #<procedure:cdr> #<procedure:car> #<procedure:car>)
Before coding the main solve function, let's define the one helper function we'll need. ends-with? accepts a suffix, and returns a predicate function indicating whether a string ends with the specified suffix:
#t
The solve function is straightforward. It accepts a goal? predicate and a starting key. It will then cycle through the directions, and do the following:
- Retrieve the accessor function (
carorcdr) from thedirectionslist - Retrieve the
conscell from thehashassociated with the key getthe left or right value from theconscell (usingcarorcdr)- If the value satisfies the
goal?predicate, return the number ofstepswe used - Otherwise, iterate by:
- Set the key to the value we retrieved
- Increment the number of steps
- Retrieve the next direction (wrapping around to the beginning of the list if necessary)
(define (solve goal? key)
(for/fold ([ key key ]
[ steps 0 ]
#:result steps)
([ get (in-cycle directions) ])
#:break (goal? key)
(values (get (hash-ref nodes key)) (add1 steps))))
Part 1 simply calls solve with a goal? predicate and a starting key:
20777
For Part 2, we need to "simultaneously" navigate from multiple starting keys until they all satisfy the goal? predicate. This is a classic Advent of Code puzzle where the algorithm is cyclical in nature, and the naive approach would take far too long to run.
The trick is to recognize that each starting key will loop to the goal key every N steps, so we need to figure out the number of steps when they all sync up. For example, let's say one key repeats every 2 steps, and another key repeats every 3 steps, when will they both reach the goal at the same time?
We see that after 6 steps, they're both back to 1. The mathematical operation that represents this behavior is the "least common multiple", or lcm. Here is a wikipedia article.
So, we need to compute the number of steps for each of the keys individually, and compute the lcm of all of the results. The algorithm is as follows:
- Line 2: get a list of all the keys in the
hash - Line 3: filter the list to only ones ending in "A"
- Line 4: for each key, call
solvewith agoal?of "ends with Z" - Line 5: compute the
lcmon the resulting list of steps
(define (part2)
(~> (hash-keys nodes)
(filter (ends-with? "A") _)
(map (curry solve (ends-with? "Z")) _)
(apply lcm _)))
(part2)
13289612809129