diff --git a/2023/Cargo.lock b/2023/Cargo.lock index a07d952..ac90d14 100644 --- a/2023/Cargo.lock +++ b/2023/Cargo.lock @@ -152,6 +152,16 @@ dependencies = [ "rstest", ] +[[package]] +name = "day-16" +version = "2023.0.0" +dependencies = [ + "glam", + "itertools", + "nom", + "nom_locate", +] + [[package]] name = "day-2" version = "2023.0.0" diff --git a/2023/day-16/Cargo.toml b/2023/day-16/Cargo.toml new file mode 100644 index 0000000..f6284ef --- /dev/null +++ b/2023/day-16/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "day-16" +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 } +glam.workspace = true +nom_locate.workspace = true diff --git a/2023/day-16/src/lib.rs b/2023/day-16/src/lib.rs new file mode 100644 index 0000000..3fafe8d --- /dev/null +++ b/2023/day-16/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-16/src/main.rs b/2023/day-16/src/main.rs new file mode 100644 index 0000000..937bb5b --- /dev/null +++ b/2023/day-16/src/main.rs @@ -0,0 +1,12 @@ +#![warn(clippy::all, clippy::pedantic)] + +use day_16::part1; +use day_16::part2; + +fn main() { + let input = include_str!("./input.txt"); + let part1_result = part1(input); + println!("part 1: {part1_result}"); + let part2_result = part2(input); + println!("part 2: {part2_result}"); +} diff --git a/2023/day-16/src/part1.rs b/2023/day-16/src/part1.rs new file mode 100644 index 0000000..b0414ac --- /dev/null +++ b/2023/day-16/src/part1.rs @@ -0,0 +1,162 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::collections::{HashMap, HashSet, VecDeque}; + +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>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Gadget { + Horizontal, + Vertical, + UlDr, + UrDl, + None, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum FromDir { + Left, + Right, + Up, + Down, +} + +/// day 16 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 +#[must_use] +pub fn part1(input: &str) -> String { + let input = Span::new(input); + let (_, (gadgets, maxes)) = parse_input(input).expect("always aoc"); + let mut movement_cache = HashSet::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::from([(IVec2::new(0, 0), FromDir::Left)]); + while !queue.is_empty() { + let pos = queue.pop_front().unwrap(); + if !movement_cache.insert(pos) { + continue; // cycle detection + } + let (pos, from) = pos; + if pos.x >= maxes.x || pos.x < 0 || pos.y >= maxes.y || pos.y < 0 { + continue; //outside grid + } + visited.insert(pos); + if let Some(gadget) = gadgets.get(&pos) { + match (gadget, from) { + (Gadget::Horizontal, FromDir::Left) => { + queue.push_back((pos + IVec2::new(1, 0), from)); + } + (Gadget::Horizontal, FromDir::Right) => { + queue.push_back((pos + IVec2::new(-1, 0), from)); + } + (Gadget::Horizontal, FromDir::Up | FromDir::Down) => { + queue.push_back((pos + IVec2::new(1, 0), FromDir::Left)); + queue.push_back((pos + IVec2::new(-1, 0), FromDir::Right)); + } + (Gadget::Vertical, FromDir::Up) => queue.push_back((pos + IVec2::new(0, 1), from)), + (Gadget::Vertical, FromDir::Down) => { + queue.push_back((pos + IVec2::new(0, -1), from)); + } + (Gadget::Vertical, FromDir::Left | FromDir::Right) => { + queue.push_back((pos + IVec2::new(0, 1), FromDir::Up)); + queue.push_back((pos + IVec2::new(0, -1), FromDir::Down)); + } + (Gadget::UlDr, FromDir::Up) | (Gadget::UrDl, FromDir::Down) => { + queue.push_back((pos + IVec2::new(1, 0), FromDir::Left)); + } + (Gadget::UlDr, FromDir::Down) | (Gadget::UrDl, FromDir::Up) => { + queue.push_back((pos + IVec2::new(-1, 0), FromDir::Right)); + } + (Gadget::UlDr, FromDir::Left) | (Gadget::UrDl, FromDir::Right) => { + queue.push_back((pos + IVec2::new(0, 1), FromDir::Up)); + } + (Gadget::UlDr, FromDir::Right) | (Gadget::UrDl, FromDir::Left) => { + queue.push_back((pos + IVec2::new(0, -1), FromDir::Down)); + } + _ => unimplemented!("This should never happen"), + }; + } else { + let next_pos = pos + + match from { + FromDir::Left => IVec2::new(1, 0), + FromDir::Right => IVec2::new(-1, 0), + FromDir::Up => IVec2::new(0, 1), + FromDir::Down => IVec2::new(0, -1), + }; + queue.push_back((next_pos, from)); + } + } + visited.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_gadget(input: Span) -> IResult { + alt(( + tag("-").map(with_xy).map(|x| (x.extra, Gadget::Horizontal)), + tag("|").map(with_xy).map(|x| (x.extra, Gadget::Vertical)), + tag(r"\").map(with_xy).map(|x| (x.extra, Gadget::UlDr)), + tag(r"/").map(with_xy).map(|x| (x.extra, Gadget::UrDl)), + tag(".").map(with_xy).map(|x| (x.extra, Gadget::None)), + ))(input) +} + +fn parse_input(input: Span) -> IResult, IVec2)> { + let (input, (gadgets, max_x, max_y)) = fold_many1( + terminated(many1(parse_gadget), alt((complete::line_ending, eof))), + || (HashMap::new(), 0, 0), + |(mut acc, _, max_y), row| { + let max_x = row.len().try_into().unwrap(); + row.into_iter() + .filter(|(_, gadget)| *gadget != Gadget::None) + .for_each(|(pos, gadget)| { + acc.insert(pos, gadget); + }); + (acc, max_x, max_y + 1) + }, + )(input)?; + Ok((input, (gadgets, IVec2::new(max_x, max_y)))) +} + +#[cfg(test)] +mod test { + use super::*; + + const INPUT: &str = r".|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|...."; + + #[test] + fn part1_works() { + let result = part1(INPUT); + assert_eq!(result, "46".to_string()); + } +} diff --git a/2023/day-16/src/part2.rs b/2023/day-16/src/part2.rs new file mode 100644 index 0000000..cd3524f --- /dev/null +++ b/2023/day-16/src/part2.rs @@ -0,0 +1,187 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::collections::{HashMap, HashSet, VecDeque}; + +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>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Gadget { + Horizontal, + Vertical, + UlDr, + UrDl, + None, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum FromDir { + Left, + Right, + Up, + Down, +} + +/// day 16 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 +#[must_use] +pub fn part2(input: &str) -> String { + let input = Span::new(input); + let (_, (gadgets, maxes)) = parse_input(input).expect("always aoc"); + (0..maxes.x) + .flat_map(|x| { + [ + (IVec2::new(x, 0), FromDir::Up), + (IVec2::new(x, maxes.y - 1), FromDir::Down), + ] + }) + .chain((0..maxes.y).flat_map(|y| { + [ + (IVec2::new(0, y), FromDir::Left), + (IVec2::new(maxes.x - 1, y), FromDir::Right), + ] + })) + .map(|(start_pos, from)| check_from_start(start_pos, from, &gadgets, maxes)) + .max() + .unwrap() + .to_string() +} + +fn check_from_start( + start_pos: IVec2, + from: FromDir, + gadgets: &HashMap, + maxes: IVec2, +) -> usize { + let mut movement_cache = HashSet::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::from([(start_pos, from)]); + while !queue.is_empty() { + let pos = queue.pop_front().unwrap(); + if !movement_cache.insert(pos) { + continue; // cycle detection + } + let (pos, from) = pos; + if pos.x >= maxes.x || pos.x < 0 || pos.y >= maxes.y || pos.y < 0 { + continue; //outside grid + } + visited.insert(pos); + if let Some(gadget) = gadgets.get(&pos) { + match (gadget, from) { + (Gadget::Horizontal, FromDir::Left) => { + queue.push_back((pos + IVec2::new(1, 0), from)); + } + (Gadget::Horizontal, FromDir::Right) => { + queue.push_back((pos + IVec2::new(-1, 0), from)); + } + (Gadget::Horizontal, FromDir::Up | FromDir::Down) => { + queue.push_back((pos + IVec2::new(1, 0), FromDir::Left)); + queue.push_back((pos + IVec2::new(-1, 0), FromDir::Right)); + } + (Gadget::Vertical, FromDir::Up) => queue.push_back((pos + IVec2::new(0, 1), from)), + (Gadget::Vertical, FromDir::Down) => { + queue.push_back((pos + IVec2::new(0, -1), from)); + } + (Gadget::Vertical, FromDir::Left | FromDir::Right) => { + queue.push_back((pos + IVec2::new(0, 1), FromDir::Up)); + queue.push_back((pos + IVec2::new(0, -1), FromDir::Down)); + } + (Gadget::UlDr, FromDir::Up) | (Gadget::UrDl, FromDir::Down) => { + queue.push_back((pos + IVec2::new(1, 0), FromDir::Left)); + } + (Gadget::UlDr, FromDir::Down) | (Gadget::UrDl, FromDir::Up) => { + queue.push_back((pos + IVec2::new(-1, 0), FromDir::Right)); + } + (Gadget::UlDr, FromDir::Left) | (Gadget::UrDl, FromDir::Right) => { + queue.push_back((pos + IVec2::new(0, 1), FromDir::Up)); + } + (Gadget::UlDr, FromDir::Right) | (Gadget::UrDl, FromDir::Left) => { + queue.push_back((pos + IVec2::new(0, -1), FromDir::Down)); + } + _ => unimplemented!("This should never happen"), + }; + } else { + let next_pos = pos + + match from { + FromDir::Left => IVec2::new(1, 0), + FromDir::Right => IVec2::new(-1, 0), + FromDir::Up => IVec2::new(0, 1), + FromDir::Down => IVec2::new(0, -1), + }; + queue.push_back((next_pos, from)); + } + } + visited.len() +} + +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_gadget(input: Span) -> IResult { + alt(( + tag("-").map(with_xy).map(|x| (x.extra, Gadget::Horizontal)), + tag("|").map(with_xy).map(|x| (x.extra, Gadget::Vertical)), + tag(r"\").map(with_xy).map(|x| (x.extra, Gadget::UlDr)), + tag(r"/").map(with_xy).map(|x| (x.extra, Gadget::UrDl)), + tag(".").map(with_xy).map(|x| (x.extra, Gadget::None)), + ))(input) +} + +fn parse_input(input: Span) -> IResult, IVec2)> { + let (input, (gadgets, max_x, max_y)) = fold_many1( + terminated(many1(parse_gadget), alt((complete::line_ending, eof))), + || (HashMap::new(), 0, 0), + |(mut acc, _, max_y), row| { + let max_x = row.len().try_into().unwrap(); + row.into_iter() + .filter(|(_, gadget)| *gadget != Gadget::None) + .for_each(|(pos, gadget)| { + acc.insert(pos, gadget); + }); + (acc, max_x, max_y + 1) + }, + )(input)?; + Ok((input, (gadgets, IVec2::new(max_x, max_y)))) +} + +#[cfg(test)] +mod test { + use super::*; + + const INPUT: &str = r".|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|...."; + + #[test] + fn part2_works() { + let result = part2(INPUT); + assert_eq!(result, "51".to_string()); + } +}