diff --git a/input/08-test-1 b/input/08-test-1 new file mode 100644 index 0000000..34ffa8a --- /dev/null +++ b/input/08-test-1 @@ -0,0 +1,5 @@ +LLR + +AAA = (BBB, BBB) +BBB = (AAA, ZZZ) +ZZZ = (ZZZ, ZZZ) \ No newline at end of file diff --git a/input/08-test-2 b/input/08-test-2 new file mode 100644 index 0000000..a8e2c98 --- /dev/null +++ b/input/08-test-2 @@ -0,0 +1,10 @@ +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) \ No newline at end of file diff --git a/src/bin/08.rs b/src/bin/08.rs new file mode 100644 index 0000000..86c5e77 --- /dev/null +++ b/src/bin/08.rs @@ -0,0 +1,144 @@ +use aoc2023::lcm; +use std::collections::HashMap; + +const INPUT: &str = include_str!("../../input/08"); +const TEST_INPUT_1: &str = include_str!("../../input/08-test-1"); +const TEST_INPUT_2: &str = include_str!("../../input/08-test-2"); + +fn main() { + assert_eq!(part1(TEST_INPUT_1), 6, "Part 1"); + println!("Part 1: {}", part1(INPUT)); + assert_eq!(part2(TEST_INPUT_2), 6, "Part 2"); + println!("Part 2: {}", part2(INPUT)); +} + +fn part1(input: &str) -> usize { + let start = "AAA".to_string(); + let end = |location: &str| location == "ZZZ"; + Map::parse(input).count_steps(start, end) +} + +fn part2(input: &str) -> usize { + let map = Map::parse(input); + let end = |location: &str| location.ends_with('Z'); + + map.starting_locations() + .map(|start| map.count_steps(start, end)) + .reduce(lcm) + .unwrap() +} + +struct Map { + transitions: HashMap, + instructions: Vec, +} + +impl Map { + /// Apply a list of instructions. + fn follow(&self, from: String) -> String { + self.instructions + .iter() + .fold(from, |location, &instruction| { + self.step(location, instruction) + }) + } + + /// Apply a single instruction. + fn step(&self, from: String, instruction: Instruction) -> String { + let direction = self.transitions.get(&from).unwrap(); + match instruction { + Instruction::Left => direction.left.clone(), + Instruction::Right => direction.right.clone(), + } + } + + fn starting_locations(&self) -> impl Iterator + '_ { + self.transitions + .keys() + .filter(|k| k.ends_with('A')) + .map(|k| k.to_string()) + } + + fn count_steps(&self, start: String, end: impl Fn(&str) -> bool) -> usize { + let mut location = start; + let mut steps = 0; + + while !end(&location) { + location = self.follow(location); + steps += self.instructions.len(); + } + + steps + } + + fn parse(input: &str) -> Self { + let mut lines = input.lines(); + let instructions = lines + .next() + .unwrap() + .chars() + .map(Instruction::parse) + .collect(); + let transitions = lines + .skip(1) + .map(Transition::parse) + .map(|t| (t.from, t.directions)) + .collect(); + Self { + instructions, + transitions, + } + } +} + +struct Directions { + left: String, + right: String, +} + +impl Directions { + fn parse(tuple: &str) -> Self { + let (left, right) = tuple + .strip_prefix('(') + .unwrap() + .strip_suffix(')') + .unwrap() + .split_once(", ") + .unwrap(); + Self { + left: left.to_string(), + right: right.to_string(), + } + } +} + +#[derive(Copy, Clone)] +enum Instruction { + Left, + Right, +} + +impl Instruction { + fn parse(c: char) -> Self { + match c { + 'L' => Self::Left, + 'R' => Self::Right, + other => panic!("unknown instruction '{other}'"), + } + } +} + +struct Transition { + from: String, + directions: Directions, +} + +impl Transition { + fn parse(line: &str) -> Self { + let (from, directions) = line.split_once(" = ").unwrap(); + Self { + from: from.to_string(), + directions: Directions::parse(directions), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cac850f..6673bea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ -use std::fmt::{Debug, Display}; -use std::str::FromStr; +use std::{ + fmt::{Debug, Display}, + ops::{Div, Mul, Rem}, + str::FromStr, +}; /// Parse a whitespace separated list of things. /// @@ -27,3 +30,46 @@ where { format!("{a}{b}").parse().unwrap() } + +/// Return the greatest common divisor. +/// +/// https://en.wikipedia.org/wiki/Euclidean_algorithm +pub fn gcd(mut a: T, mut b: T) -> T +where + T: PartialEq + Default + Rem + Copy, +{ + let zero = T::default(); + while b != zero { + (a, b) = (b, a % b); + } + + a +} + +/// Return the least common multiple. +/// +/// https://en.wikipedia.org/wiki/Least_common_multiple +pub fn lcm(a: T, b: T) -> T +where + T: PartialEq + Default + Rem + Div + Mul + Copy, +{ + let gcd = gcd(a, b); + a / gcd * b +} + +#[cfg(test)] +mod test { + use crate::*; + + #[test] + fn test_gcd() { + assert_eq!(gcd(252, 105), 21); + assert_eq!(gcd(105, 252), 21); + } + + #[test] + fn test_lcm() { + assert_eq!(lcm(4, 6), 12); + assert_eq!(lcm(6, 4), 12); + } +}