Advent of Code 2023 - Day 8
First day of week #2, let’s take a look at day 8
You can find the final code @ advent-of-code/2023/day8
We are given the following input
RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)
Starting with AAA, you need to look up the next element based on the next left/right instruction in your input.
If you run out of left/right instructions, repeat the whole sequence of instructions as necessary
The input could be seen as a graph that we have to navigate, starting from
AAA
, following the first line instructions until we end up at ZZZ
.
I would like to implement some kind of state machine that keeps track of the
current position and the graph model in a HashMap
.
#[derive(Debug)]
struct MapModel {
pos: String,
map: HashMap<String, (String, String)>,
}
To parse the input we could make use of a regex, let’s add the crate to our
program with cargo add regex once_cell
. We can now define our regex and
parsing logic of our MapModel
type.
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([A-Z]{3}) = \(([A-Z]{3}), ([A-Z]{3})\)").unwrap());
impl From<&str> for MapModel {
fn from(value: &str) -> Self {
let mut map = HashMap::new();
for line in value.lines() {
let captures = match RE.captures(&line) {
None => panic!("cannot parse line"),
Some(captures) => captures,
};
let key = captures[1].to_string();
let left = captures[2].to_string();
let right = captures[3].to_string();
map.insert(key, (left, right));
}
MapModel {
pos: String::from("AAA"),
map,
}
}
}
We have the base layer of the implementation, let’s now define two methods that
are going to change the pos
of the state machine by taking the left or right
path of the graph at the current position.
impl MapModel {
fn left(&mut self) {
let (l, _) = self.map.get(&self.pos).unwrap();
self.pos = l.to_owned();
}
fn right(&mut self) {
let (_, r) = self.map.get(&self.pos).unwrap();
self.pos = r.to_owned();
}
}
We now just have to iterate over the first line instructions until we reach ZZZ
, here’s part 1 solution
fn part1(input: &str) -> io::Result<u32> {
let (track, map) = input.split_once("\n\n").unwrap();
let track: Vec<char> = track.chars().collect();
let mut map = MapModel::from(map);
let mut i = 0;
loop {
match track[i % track.len()] {
'R' => map.right(),
'L' => map.left(),
_ => panic!("invalid move"),
};
if map.pos == "ZZZ" {
return Ok(i as u32 + 1);
}
i += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(
2,
part1(
r"RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)",
)
.unwrap()
)
}
}
And that’s it for part 1, pretty straightforward. Let’s jump on part 2!
Part 2 states that we now should start simultaneously to every node that ends
with A
and we should advance every node following the first line instructions,
the program should stop when every node that we end up on ends with Z
.
There’s little logic to change from part 1, we can reuse most of it. To find the
solution to this problem it is obvious that every node has to cycle back at some
point after reaching the nodes ending with Z
(you can test this and you’ll
notice if it’s not), we therefore have to find the length of each cycle
and at that point we know that each node is going to end up on nodes ending with
Z
at the least common multiple of all those values that we’ve found.
Let’s run 'cargo add lcmx' so that we don’t have to code the least common
multiple function. With that, we can calculate the length of the cycle by
calculating the total steps moving from the intial node ending with A
to the
final node ending with Z
.
fn part2(input: &str) -> io::Result<u64> {
let (track, map) = input.split_once("\n\n").unwrap();
let track: Vec<char> = track.chars().collect();
let mut map = MapModel::from(map);
let mut starting_pos: Vec<String> = Vec::new();
for k in map.map.keys().filter(|x| x.ends_with("A")) {
starting_pos.push(k.to_owned());
}
let mut steps: Vec<u64> = Vec::new();
for sp in starting_pos {
map.pos = sp;
let mut i = 0;
loop {
match track[i % track.len()] {
'R' => map.right(),
'L' => map.left(),
_ => panic!("invalid move"),
};
if map.pos.ends_with("Z") {
steps.push(i as u64 + 1);
break;
}
i += 1;
}
}
Ok(lcmx(&steps).unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
6,
part2(
r"LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)",
)
.unwrap()
)
}
}
And that gives us the correct answer. I’ve spend almost half an hour trying to
understand why the answer to the problem was wrong and always too low, just to
find out that lcmx
overflowed u32
type, so make sure to use u64
in this
case!