diff --git a/2023/Cargo.lock b/2023/Cargo.lock index e21e5cf..7c36fab 100644 --- a/2023/Cargo.lock +++ b/2023/Cargo.lock @@ -210,6 +210,17 @@ dependencies = [ "rstest", ] +[[package]] +name = "day-21" +version = "2023.0.0" +dependencies = [ + "glam", + "itertools", + "nom", + "nom_locate", + "rstest", +] + [[package]] name = "day-3" version = "2023.0.0" diff --git a/2023/day-21/Cargo.toml b/2023/day-21/Cargo.toml new file mode 100644 index 0000000..52de53a --- /dev/null +++ b/2023/day-21/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "day-21" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nom = { workspace = true } +itertools = {workspace = true } +nom_locate.workspace = true +glam.workspace = true + +[dev-dependencies] +rstest.workspace = true diff --git a/2023/day-21/src/lib.rs b/2023/day-21/src/lib.rs new file mode 100644 index 0000000..3fafe8d --- /dev/null +++ b/2023/day-21/src/lib.rs @@ -0,0 +1,4 @@ +pub mod part1; +pub use crate::part1::*; +pub mod part2; +pub use crate::part2::*; diff --git a/2023/day-21/src/main.rs b/2023/day-21/src/main.rs new file mode 100644 index 0000000..eada3d3 --- /dev/null +++ b/2023/day-21/src/main.rs @@ -0,0 +1,12 @@ +#![warn(clippy::all, clippy::pedantic)] + +use day_21::part1; +use day_21::part2; + +fn main() { + let input = include_str!("./input.txt"); + let part1_result = part1(input, 64); + println!("part 1: {part1_result}"); + let part2_result = part2(input, 26_501_365); + println!("part 2: {part2_result}"); +} diff --git a/2023/day-21/src/part1.rs b/2023/day-21/src/part1.rs new file mode 100644 index 0000000..921c247 --- /dev/null +++ b/2023/day-21/src/part1.rs @@ -0,0 +1,101 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::collections::HashSet; + +use glam::IVec2; +use itertools::Itertools; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete, + combinator::eof, + multi::{fold_many1, many1}, + sequence::terminated, + IResult, Parser, +}; +use nom_locate::LocatedSpan; + +type Span<'a> = LocatedSpan<&'a str>; +type SpanIVec2<'a> = LocatedSpan<&'a str, IVec2>; + +fn next_step(loc: IVec2, boulders: &HashSet) -> Vec { + [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] + .iter() + .map(|dir| loc + *dir) + .filter(|loc| boulders.get(loc).is_none()) + .collect() +} + +/// day 21 part 1 of aoc 2023 +/// +/// # Arguments +/// - input the input for today's puzzle +/// +/// # Panics +/// panics whne it cannot parse the input OR when ever the number of game numbers is greater than +/// usize +#[must_use] +pub fn part1(input: &str, steps: u32) -> String { + let (_, (start, boulders)) = parse_input(Span::from(input)).expect("AOC input should be valid"); + let mut current = [start].into_iter().collect::>(); + for _i in 0..steps { + current = current + .iter() + .flat_map(|loc| next_step(*loc, &boulders)) + .unique() + .collect::>(); + } + current.len().to_string() +} + +fn with_xy(span: Span) -> SpanIVec2 { + let x = i32::try_from(span.get_column()).expect("overflow") - 1; + let y = i32::try_from(span.location_line()).expect("wrap around") - 1; + span.map_extra(|()| IVec2::new(x, y)) +} + +fn parse_input(input: Span) -> IResult)> { + fold_many1( + terminated( + many1(alt((tag("S"), tag("."), tag("#"))).map(with_xy)), + alt((complete::line_ending, eof)), + ), + || (IVec2::splat(0), HashSet::new()), + |(mut start, mut set), row| { + for spot in row { + if spot.fragment() == &"S" { + start = spot.extra; + } + if spot.fragment() == &"#" { + set.insert(spot.extra); + } + } + (start, set) + }, + )(input) +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + const INPUT: &str = "........... +.....###.#. +.###.##..#. +..#.#...#.. +....#.#.... +.##..S####. +.##..#...#. +.......##.. +.##.#.####. +.##..##.##. +..........."; + #[rstest] + #[case(6, "16")] + fn part1_works(#[case] steps: u32, #[case] expected: &str) { + let result = part1(INPUT, steps); + assert_eq!(result, expected.to_string()); + } +} diff --git a/2023/day-21/src/part2.rs b/2023/day-21/src/part2.rs new file mode 100644 index 0000000..da7b514 --- /dev/null +++ b/2023/day-21/src/part2.rs @@ -0,0 +1,128 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::{collections::HashSet, ops::Not}; + +use glam::IVec2; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete, + combinator::eof, + multi::{fold_many1, many1}, + sequence::terminated, + IResult, Parser, +}; +use nom_locate::LocatedSpan; + +type Span<'a> = LocatedSpan<&'a str>; +type SpanIVec2<'a> = LocatedSpan<&'a str, IVec2>; + +fn next_step( + loc: IVec2, + size: IVec2, + boulders: &HashSet, +) -> impl Iterator + '_ { + [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] + .iter() + .map(move |dir| loc + *dir) + .filter(move |loc| boulders.contains(&(loc.rem_euclid(size))).not()) +} + +/// day 21 part 2 of aoc 2023 +/// +/// # Arguments +/// - input the input for today's puzzle +/// +/// # Panics +/// panics whne it cannot parse the input OR when ever the number of game numbers is greater than +/// usize +#[must_use] +pub fn part2(input: &str, steps: usize) -> String { + let (_, (start, size, boulders)) = + parse_input(Span::from(input)).expect("AOC input should be valid"); + let sq_size = usize::try_from(size.x).unwrap(); + let base = steps % sq_size; + let reps = steps / sq_size; + let mut current = [start].into_iter().collect::>(); + let mut coef = Vec::new(); + for i in 0..=(base + sq_size * 2 + 1) { + current = current + .iter() + .flat_map(|loc| next_step(*loc, size, &boulders)) + .collect::>(); + + if i >= base - 1 && (i - base + 1) % sq_size == 0 { + //println!("{i} - {} - {}", (i - base) / sq_size, current.len()); + coef.push(current.len()); + } + } + + //TODO assuming this is fit with a quadratic + let a = (coef[2] - 2 * coef[1] + coef[0]) / 2; + let b = coef[1] - coef[0] - a; + let c = coef[0]; + + let total = a * reps.pow(2) + b * reps + c; + + total.to_string() + // TODO this doesn't work for general case +} + +fn with_xy(span: Span) -> SpanIVec2 { + let x = i32::try_from(span.get_column()).expect("overflow") - 1; + let y = i32::try_from(span.location_line()).expect("wrap around") - 1; + span.map_extra(|()| IVec2::new(x, y)) +} + +fn parse_input(input: Span) -> IResult)> { + fold_many1( + terminated( + many1(alt((tag("S"), tag("."), tag("#"))).map(with_xy)), + alt((complete::line_ending, eof)), + ), + || (IVec2::splat(0), IVec2::splat(0), HashSet::new()), + |(mut start, mut size, mut set), row| { + for spot in row { + if spot.fragment() == &"S" { + start = spot.extra; + } + if spot.fragment() == &"#" { + set.insert(spot.extra); + } + size = size.max(spot.extra + 1); + } + (start, size, set) + }, + )(input) +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + const INPUT: &str = "........... +.....###.#. +.###.##..#. +..#.#...#.. +....#.#.... +.##..S####. +.##..#...#. +.......##.. +.##.#.####. +.##..##.##. +..........."; + #[rstest] + #[case(6, "16")] + #[case(10, "50")] + #[case(50, "1594")] + #[case(100, "6536")] + #[case(500, "167004")] + #[case(1000, "668697")] + #[case(5000, "16733044")] + fn part2_works(#[case] steps: usize, #[case] expected: &str) { + let result = part2(INPUT, steps); + assert_eq!(result, expected.to_string()); + } +}