From 387e8f62d09f29fb49101d01ad776532effb85a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Fri, 8 Dec 2023 15:25:21 +0100 Subject: [PATCH] Add day 8 --- 2023/all.sh | 4 + 2023/day8/.gitignore | 1 + 2023/day8/Cargo.lock | 39 +++++++ 2023/day8/Cargo.toml | 10 ++ 2023/day8/src/main.rs | 261 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 315 insertions(+) create mode 100644 2023/day8/.gitignore create mode 100644 2023/day8/Cargo.lock create mode 100644 2023/day8/Cargo.toml create mode 100644 2023/day8/src/main.rs diff --git a/2023/all.sh b/2023/all.sh index 35e7776..f2531fe 100755 --- a/2023/all.sh +++ b/2023/all.sh @@ -31,6 +31,10 @@ run() { echo "day7" ./day7/target/release/day7 1 ./day7/target/release/day7 2 + + echo "day8" + ./day8/target/release/day8 1 + ./day8/target/release/day8 2 } time run diff --git a/2023/day8/.gitignore b/2023/day8/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/2023/day8/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/2023/day8/Cargo.lock b/2023/day8/Cargo.lock new file mode 100644 index 0000000..b797e5a --- /dev/null +++ b/2023/day8/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day8" +version = "0.1.0" +dependencies = [ + "indoc", + "nom", +] + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] diff --git a/2023/day8/Cargo.toml b/2023/day8/Cargo.toml new file mode 100644 index 0000000..f5d158c --- /dev/null +++ b/2023/day8/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "day8" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +indoc = "2.0.4" +nom = "7.1.3" diff --git a/2023/day8/src/main.rs b/2023/day8/src/main.rs new file mode 100644 index 0000000..e407cb6 --- /dev/null +++ b/2023/day8/src/main.rs @@ -0,0 +1,261 @@ +use std::collections::HashMap; + +use nom::{ + character::complete::{anychar, char, multispace0, newline, one_of}, + combinator::map, + multi::{many1, separated_list1}, + sequence::{delimited, separated_pair, tuple}, + IResult, +}; + +fn lcm(numbers: impl Iterator) -> usize { + fn lcm(a: usize, b: usize) -> usize { + fn gcd(a: usize, b: usize) -> usize { + if b == 0 { + a + } else if a == 0 { + b + } else if a > b { + gcd(a - b, b) + } else { + gcd(a, b - a) + } + } + a / gcd(a, b) * b + } + + numbers.reduce(lcm).unwrap() +} + +trait Parse { + type Out; + fn parse(s: &str) -> IResult<&str, Self::Out>; +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] +struct Node(char, char, char); + +fn spaces(i: &str) -> IResult<&str, Vec> { + many1(char(' '))(i) +} + +impl Parse for Node { + type Out = Self; + + fn parse(s: &str) -> IResult<&str, Self::Out> { + let (rest, element) = tuple((anychar, anychar, anychar))(s)?; + Ok((rest, element.into())) + } +} + +impl TryFrom<&str> for Node { + type Error = String; + fn try_from(value: &str) -> Result { + let chars = value.chars().take(3).collect::>(); + Ok(Self(chars[0], chars[1], chars[2])) + } +} + +impl From<(char, char, char)> for Node { + fn from((a, b, c): (char, char, char)) -> Self { + Self(a, b, c) + } +} + +impl Node { + fn ends_with(&self, c: char) -> bool { + self.2 == c + } +} + +#[derive(Debug)] +struct NodeEdges(HashMap); + +impl Parse for NodeEdges { + type Out = Self; + + fn parse(s: &str) -> IResult<&str, Self::Out> { + let (rest, edges) = separated_list1( + newline, + separated_pair( + Node::parse, + tuple((spaces, char('='), spaces)), + delimited( + char('('), + separated_pair(Node::parse, tuple((char(','), spaces)), Node::parse), + char(')'), + ), + ), + )(s)?; + let edges: HashMap = HashMap::from_iter(edges); + Ok((rest, Self(edges))) + } +} + +#[derive(Debug, Clone, Copy)] +enum Direction { + Left, + Right, +} + +impl TryFrom for Direction { + type Error = String; + + fn try_from(value: char) -> Result { + Ok(match value { + 'R' => Self::Right, + 'L' => Self::Left, + _ => Err(format!("unknow direction {value}"))?, + }) + } +} + +impl Parse for Direction { + type Out = Self; + + fn parse(s: &str) -> IResult<&str, Self::Out> { + let (rest, c) = map(one_of("RL"), |f: char| f.try_into().unwrap())(s)?; + Ok((rest, c)) + } +} + +#[derive(Debug)] +struct Map { + directions: Vec, + edges: NodeEdges, +} + +impl Parse for Map { + type Out = Self; + + fn parse(s: &str) -> IResult<&str, Self::Out> { + let (rest, (directions, edges)) = delimited( + multispace0, + separated_pair(many1(Direction::parse), many1(newline), NodeEdges::parse), + multispace0, + )(s)?; + Ok((rest, Self { directions, edges })) + } +} + +fn part1(input: &str) -> Result { + let (rest, map) = Map::parse(input).map_err(|e| e.to_string())?; + assert!(rest.is_empty()); + + let start: Node = "AAA".try_into().unwrap(); + let goal: Node = "ZZZ".try_into().unwrap(); + + let mut current_node = start; + + let mut steps = 0; + for direction in map.directions.into_iter().cycle() { + if current_node == goal { + break; + } + let new_node = match direction { + Direction::Left => map.edges.0[¤t_node].0, + Direction::Right => map.edges.0[¤t_node].1, + }; + current_node = new_node; + steps += 1; + } + Ok(steps) +} + +fn part2(input: &str) -> Result { + let (rest, map) = Map::parse(input).map_err(|e| e.to_string())?; + assert!(rest.is_empty()); + + let node_chain_lengths = map + .edges + .0 + .keys() + .filter(|node| node.ends_with('A')) + .map(|node| { + let mut depth = 1; + let mut node = *node; + for direction in map.directions[..].iter().cycle() { + let new_node = match direction { + Direction::Left => map.edges.0[&node].0, + Direction::Right => map.edges.0[&node].1, + }; + if new_node.ends_with('Z') { + break; + } + node = new_node; + depth += 1; + } + + depth + }); + + Ok(lcm(node_chain_lengths)) +} + +fn main() -> Result<(), String> { + let input = include_str!("../input"); + + let args = std::env::args().skip(1).collect::>(); + let part = args[0].parse::().unwrap(); + + if part == 1 { + println!("Part 1 : {}", part1(input)?); + } else if part == 2 { + println!("Part 2 : {}", part2(input)?); + } else { + return Err("unknown part".into()); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + + #[test] + fn part_1_example_1() { + let input = indoc! {" + RL + + AAA = (BBB, CCC) + BBB = (DDD, EEE) + CCC = (ZZZ, GGG) + DDD = (DDD, DDD) + EEE = (EEE, EEE) + GGG = (GGG, GGG) + ZZZ = (ZZZ, ZZZ) + "}; + assert_eq!(part1(input).unwrap(), 2); + } + + #[test] + fn part_1_example_2() { + let input = indoc! {" + LLR + + AAA = (BBB, BBB) + BBB = (AAA, ZZZ) + ZZZ = (ZZZ, ZZZ) + "}; + assert_eq!(part1(input).unwrap(), 6); + } + + #[test] + fn part_2_example_1() { + let input = indoc! {" + 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) + "}; + assert_eq!(part2(input).unwrap(), 6); + } +}