diff --git a/2023/Cargo.lock b/2023/Cargo.lock index 4760b1c..5f816a0 100644 --- a/2023/Cargo.lock +++ b/2023/Cargo.lock @@ -230,6 +230,17 @@ dependencies = [ "nom", ] +[[package]] +name = "day-23" +version = "2023.0.0" +dependencies = [ + "glam", + "itertools", + "nom", + "nom_locate", + "petgraph", +] + [[package]] name = "day-3" version = "2023.0.0" @@ -733,6 +744,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.13" diff --git a/2023/day-23/Cargo.toml b/2023/day-23/Cargo.toml new file mode 100644 index 0000000..2651baf --- /dev/null +++ b/2023/day-23/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "day-23" +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 +petgraph = "0.6.4" diff --git a/2023/day-23/src/lib.rs b/2023/day-23/src/lib.rs new file mode 100644 index 0000000..3fafe8d --- /dev/null +++ b/2023/day-23/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-23/src/main.rs b/2023/day-23/src/main.rs new file mode 100644 index 0000000..05a5292 --- /dev/null +++ b/2023/day-23/src/main.rs @@ -0,0 +1,12 @@ +#![warn(clippy::all, clippy::pedantic)] + +use day_23::part1; +use day_23::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-23/src/part1.rs b/2023/day-23/src/part1.rs new file mode 100644 index 0000000..a48c077 --- /dev/null +++ b/2023/day-23/src/part1.rs @@ -0,0 +1,107 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::collections::HashMap; + +use glam::IVec2; +use petgraph::{prelude::*, algo}; + + +#[derive(Debug, Copy, Clone)] +enum PointType { + Any, + OnlyDown, + OnlyLeft, + OnlyRight, + OnlyUp, +} + +impl PointType { + fn next_possibles(self) -> Vec { + match self { + PointType::Any => vec![IVec2::X, IVec2::Y, IVec2::NEG_X, IVec2::NEG_Y], + PointType::OnlyDown => vec![IVec2::Y], + PointType::OnlyLeft => vec![IVec2::NEG_X], + PointType::OnlyRight => vec![IVec2::X], + PointType::OnlyUp => vec![IVec2::NEG_Y], + } + } +} + +#[must_use] +pub fn part1(input: &str) -> String { + let maze = parse_input(input); + //get the start position (assuming there is only one) + let start = *maze.keys().find(|pos| pos.y == 0).unwrap(); + let end = maze.keys().fold(IVec2::splat(0), | max, current| if max.y.max(current.y) == current.y { *current } else {max}); + let mut maze_graph = DiGraph::<&PointType, u32>::new(); + let node_map = maze.iter().map(|(pos, point_type)| (pos, maze_graph.add_node(point_type)) ).collect::>(); + + maze.iter().flat_map(|(pos, point_type)| { + point_type.next_possibles().iter().copied().filter_map(|dir| { + let next_pos = dir + *pos; + node_map.get(&next_pos).is_some().then(|| (node_map[pos], node_map[&next_pos], 1)) + }).collect::>() + }) + .for_each(|(a, b, weight)| { + maze_graph.add_edge(a,b,weight); + }); + + (algo::all_simple_paths::,_>(&maze_graph, node_map[&start], node_map[&end], 0, None).max_by(|a, b| a.len().cmp(&b.len())).unwrap().len() -1).to_string() +} + +fn parse_input(input: &str) -> HashMap { + input + .lines() + .enumerate() + .flat_map(|(y, row)| { + row.chars().enumerate().filter_map(move |(x, c)| { + let pos = IVec2::new(i32::try_from(x).unwrap(), i32::try_from(y).unwrap()); + match c { + '.' => Some((pos, PointType::Any)), + '>' => Some((pos, PointType::OnlyRight)), + 'v' => Some((pos, PointType::OnlyDown)), + '^' => Some((pos, PointType::OnlyUp)), + '<' => Some((pos, PointType::OnlyLeft)), + _ => None, + } + }) + }) + .collect() +} + +#[cfg(test)] +mod test { + use super::*; + + const INPUT: &str = "#.##################### +#.......#########...### +#######.#########.#.### +###.....#.>.>.###.#.### +###v#####.#v#.###.#.### +###.>...#.#.#.....#...# +###v###.#.#.#########.# +###...#.#.#.......#...# +#####.#.#.#######.#.### +#.....#.#.#.......#...# +#.#####.#.#.#########v# +#.#...#...#...###...>.# +#.#.#v#######v###.###v# +#...#.>.#...>.>.#.###.# +#####v#.#.###v#.#.###.# +#.....#...#...#.#.#...# +#.#########.###.#.#.### +#...###...#...#...#.### +###.###.#.###v#####v### +#...#...#.#.>.>.#.>.### +#.###.###.#.###.#.#v### +#.....###...###...#...# +#####################.#"; + + #[test] + fn part1_works() { + let result = part1(INPUT); + assert_eq!(result, "94".to_string()); + } +} + + diff --git a/2023/day-23/src/part2.rs b/2023/day-23/src/part2.rs new file mode 100644 index 0000000..db4a3a0 --- /dev/null +++ b/2023/day-23/src/part2.rs @@ -0,0 +1,103 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::collections::HashMap; + +use glam::IVec2; +use petgraph::{prelude::*, algo}; + + +#[derive(Debug, Copy, Clone)] +enum PointType { + Any, + OnlyDown, + OnlyLeft, + OnlyRight, + OnlyUp, +} + +impl PointType { + fn next_possibles(self) -> Vec { + match self { + _ => vec![IVec2::X, IVec2::Y, IVec2::NEG_X, IVec2::NEG_Y], + } + } +} + +#[must_use] +pub fn part2(input: &str) -> String { + let maze = parse_input(input); + //get the start position (assuming there is only one) + let start = *maze.keys().find(|pos| pos.y == 0).unwrap(); + let end = maze.keys().fold(IVec2::splat(0), | max, current| if max.y.max(current.y) == current.y { *current } else {max}); + let mut maze_graph = DiGraph::<&PointType, u32>::new(); + let node_map = maze.iter().map(|(pos, point_type)| (pos, maze_graph.add_node(point_type)) ).collect::>(); + + maze.iter().flat_map(|(pos, point_type)| { + point_type.next_possibles().iter().copied().filter_map(|dir| { + let next_pos = dir + *pos; + node_map.get(&next_pos).is_some().then(|| (node_map[pos], node_map[&next_pos], 1)) + }).collect::>() + }) + .for_each(|(a, b, weight)| { + maze_graph.add_edge(a,b,weight); + }); + + (algo::all_simple_paths::,_>(&maze_graph, node_map[&start], node_map[&end], 0, None).max_by(|a, b| a.len().cmp(&b.len())).unwrap().len() -1).to_string() +} + +fn parse_input(input: &str) -> HashMap { + input + .lines() + .enumerate() + .flat_map(|(y, row)| { + row.chars().enumerate().filter_map(move |(x, c)| { + let pos = IVec2::new(i32::try_from(x).unwrap(), i32::try_from(y).unwrap()); + match c { + '.' => Some((pos, PointType::Any)), + '>' => Some((pos, PointType::OnlyRight)), + 'v' => Some((pos, PointType::OnlyDown)), + '^' => Some((pos, PointType::OnlyUp)), + '<' => Some((pos, PointType::OnlyLeft)), + _ => None, + } + }) + }) + .collect() +} + +#[cfg(test)] +mod test { + use super::*; + + const INPUT: &str = "#.##################### +#.......#########...### +#######.#########.#.### +###.....#.>.>.###.#.### +###v#####.#v#.###.#.### +###.>...#.#.#.....#...# +###v###.#.#.#########.# +###...#.#.#.......#...# +#####.#.#.#######.#.### +#.....#.#.#.......#...# +#.#####.#.#.#########v# +#.#...#...#...###...>.# +#.#.#v#######v###.###v# +#...#.>.#...>.>.#.###.# +#####v#.#.###v#.#.###.# +#.....#...#...#.#.#...# +#.#########.###.#.#.### +#...###...#...#...#.### +###.###.#.###v#####v### +#...#...#.#.>.>.#.>.### +#.###.###.#.###.#.#v### +#.....###...###...#...# +#####################.#"; + + #[test] + fn part2_works() { + let result = part2(INPUT); + assert_eq!(result, "154".to_string()); + } +} + +