diff --git a/2023/Cargo.toml b/2023/Cargo.toml index 7000a92..877cfdf 100644 --- a/2023/Cargo.toml +++ b/2023/Cargo.toml @@ -10,6 +10,7 @@ aho-corasick = "1.1.2" anyhow = "1.0.75" clap = { version = "4.4.8", features = ["derive"] } nom = "7.1.3" +num-integer = "0.1.45" [dev-dependencies] criterion = "0.5.1" diff --git a/2023/benches/days.rs b/2023/benches/days.rs index c85789a..622a818 100644 --- a/2023/benches/days.rs +++ b/2023/benches/days.rs @@ -9,7 +9,7 @@ use criterion::Criterion; use aoc_2023::get_implementation; /// Number of days we have an implementation to benchmark -const DAYS_IMPLEMENTED: u8 = 7; +const DAYS_IMPLEMENTED: u8 = 25; fn read_input(day: u8) -> std::io::Result> { let input_path = format!("inputs/{day:02}.txt"); diff --git a/2023/src/day08.rs b/2023/src/day08.rs index 1c60e39..c93e44e 100644 --- a/2023/src/day08.rs +++ b/2023/src/day08.rs @@ -1,12 +1,15 @@ +use anyhow::Context; use nom::bytes::complete::tag; use nom::bytes::complete::take; use nom::bytes::complete::take_until; use nom::combinator::map; use nom::multi::fold_many1; +use nom::sequence::preceded; use nom::sequence::separated_pair; use nom::sequence::terminated; use nom::sequence::tuple; use nom::IResult; +use num_integer::Integer; use crate::common::parse_input; @@ -62,6 +65,25 @@ fn parse_map(i: &[u8]) -> IResult<&[u8], Map<'_>> { )(i) } +fn parse_starts(i: &[u8]) -> IResult<&[u8], Vec> { + preceded( + tuple((take_until("\n"), tag("\n\n"))), + fold_many1( + terminated( + map(take(3usize), place_to_index), + tuple((take_until("\n"), tag("\n"))), + ), + Vec::new, + |mut starts, place| { + if place % 26 == 0 { + starts.push(place) + } + starts + }, + ), + )(i) +} + pub fn part1(input: &[u8]) -> anyhow::Result { let map = parse_input(input, parse_map)?; let end = place_to_index(b"ZZZ"); @@ -78,8 +100,32 @@ pub fn part1(input: &[u8]) -> anyhow::Result { anyhow::bail!("Unreachable, loop is infinite"); } -pub fn part2(_input: &[u8]) -> anyhow::Result { - anyhow::bail!("Not implemented") +// This code is wrong. There is no reason that the start of the cycle is indeed the equal to the +// length of the cycle. But it happens to be the case, so we roll with it. Otherwise you could go +// with the full Chinese remainder theorem and knock yourself out that way. +// +// I didn't wanna. +fn find_cycle(map: &Map<'_>, start: u16) -> usize { + let mut pos = start; + for (count, &step) in map.instructions.iter().cycle().enumerate() { + if pos % 26 == 25 { + return count; + } + pos = map.transition(pos, step); + } + + unreachable!("Loop is actually infinite") +} + +pub fn part2(input: &[u8]) -> anyhow::Result { + let map = parse_input(input, parse_map)?; + let pos = parse_input(input, parse_starts)?; + + pos.iter() + .map(|&p| find_cycle(&map, p)) + .reduce(|a, b| a.lcm(&b)) + .map(|s| s.to_string()) + .context("No starting points somehow") } #[cfg(test)] @@ -88,10 +134,18 @@ mod tests { const SAMPLE: &[u8] = include_bytes!("samples/08.1.txt"); const SAMPLE2: &[u8] = include_bytes!("samples/08.2.txt"); + // N.B. sample modified because I don't want to change my parser logic to deal with ascii digits + // in addition to capitals. 1 has been replaced with D, 2 has been replaced with E. + const SAMPLE3: &[u8] = include_bytes!("samples/08.3.txt"); #[test] fn sample_part1() { assert_eq!("2", part1(SAMPLE).unwrap()); assert_eq!("6", part1(SAMPLE2).unwrap()); } + + #[test] + fn sample_part2() { + assert_eq!("6", part2(SAMPLE3).unwrap()); + } } diff --git a/2023/src/samples/08.3.txt b/2023/src/samples/08.3.txt new file mode 100644 index 0000000..a658875 --- /dev/null +++ b/2023/src/samples/08.3.txt @@ -0,0 +1,10 @@ +LR + +DDA = (DDB, XXX) +DDB = (XXX, DDZ) +DDZ = (DDB, XXX) +EEA = (EEB, XXX) +EEB = (EEC, EEC) +EEC = (EEZ, EEZ) +EEZ = (EEB, EEB) +XXX = (XXX, XXX)