diff --git a/.aoc_tiles/tiles/2024/16.png b/.aoc_tiles/tiles/2024/16.png index e5449fe..f5551a1 100644 Binary files a/.aoc_tiles/tiles/2024/16.png and b/.aoc_tiles/tiles/2024/16.png differ diff --git a/README.md b/README.md index 3aaedd1..39e18b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- 2024 - 30 ⭐ - Rust + 2024 - 31 ⭐ - Rust

@@ -47,4 +47,7 @@ + + + diff --git a/src/day16.rs b/src/day16.rs new file mode 100644 index 0000000..bb4c2cd --- /dev/null +++ b/src/day16.rs @@ -0,0 +1,181 @@ +use aoc_runner_derive::{aoc, aoc_generator}; +use grid::{AsCoord2d, Coord2d, Grid}; +use std::{ + collections::{BinaryHeap, HashMap}, + str::FromStr, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] +enum FacingDirection { + East, + South, + West, + North, +} + +impl FacingDirection { + fn ofs(&self) -> (i64, i64) { + match self { + FacingDirection::East => (1, 0), + FacingDirection::South => (0, 1), + FacingDirection::West => (-1, 0), + FacingDirection::North => (0, -1), + } + } + fn reachable(&self) -> [FacingDirection; 3] { + // Can move perpendicularly or the same direction, not backwards + match self { + FacingDirection::East | FacingDirection::West => [*self, FacingDirection::North, FacingDirection::South], + FacingDirection::South | FacingDirection::North => [*self, FacingDirection::East, FacingDirection::West], + } + } +} + +#[derive(Clone, Eq, PartialEq, Debug)] +struct State { + cost: usize, + position: Coord2d, + facing: FacingDirection, +} + +impl Ord for State { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other + .cost + .cmp(&self.cost) + .then_with(|| self.position.cmp(&other.position)) + .then_with(|| self.facing.cmp(&other.facing)) + } +} + +impl PartialOrd for State { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +struct Maze { + map: Grid, +} + +impl FromStr for Maze { + type Err = Box; + fn from_str(s: &str) -> Result { + let map: Grid = s.parse()?; + + Ok(Self { map }) + } +} + +impl Maze { + fn dijkstra(&mut self) -> usize { + let start = self.map.find(&b'S').expect("can't find start").to_coord(); + let finish = self.map.find(&b'E').expect("can't find finish").to_coord(); + + let mut distances = HashMap::new(); + let mut queue = BinaryHeap::with_capacity(self.map.data.len()); + + distances.insert((start, FacingDirection::East), 0); + queue.push(State { + cost: 0, + position: start, + facing: FacingDirection::East, + }); + + while let Some(State { cost, position, facing }) = queue.pop() { + if position == finish { + return cost; + } + + if distances.get(&(position, facing)).is_some_and(|v| cost > *v) { + continue; + } + + for (new_dir, new_position, new_cost) in facing + .reachable() + .iter() + .map(|dir| (dir, &position + dir.ofs())) + .filter(|(_, pos)| self.map.get(pos).is_some_and(|c| *c != b'#')) + .map(|(dir, pos)| (dir, pos, if *dir == facing { cost + 1 } else { cost + 1001 })) + { + if distances + .get(&(new_position, *new_dir)) + .is_none_or(|best_cost| new_cost < *best_cost) + { + queue.push(State { + cost: new_cost, + position: new_position, + facing: *new_dir, + }); + distances.insert((new_position, *new_dir), new_cost); + } + } + } + panic!("no path found"); + } +} + +fn parse(input: &str) -> Maze { + input.parse().unwrap() +} + +#[aoc(day16, part1)] +pub fn part1(input: &str) -> usize { + let mut maze = parse(input); + maze.dijkstra() +} + +#[aoc(day16, part2)] +pub fn part2(input: &str) -> i64 { + todo!() +} + +#[cfg(test)] +mod tests { + use super::*; + const EXAMPLE1: &str = "############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +###############"; + + const EXAMPLE2: &str = "################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +#################"; + + #[test] + fn part1_example() { + assert_eq!(part1(EXAMPLE1), 7036); + assert_eq!(part1(EXAMPLE2), 11048); + } + + #[test] + fn part2_example() { + assert_eq!(part2(EXAMPLE1), 0); + assert_eq!(part2(EXAMPLE2), 0); + } +} diff --git a/src/lib.rs b/src/lib.rs index 52da659..345ea54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod day16; use aoc_runner_derive::aoc_lib; pub mod day1; diff --git a/utils/grid/lib.rs b/utils/grid/lib.rs index 99baf3c..dab4207 100644 --- a/utils/grid/lib.rs +++ b/utils/grid/lib.rs @@ -7,7 +7,7 @@ use std::{ str::FromStr, }; -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Coord2d { pub x: i64, pub y: i64, @@ -126,7 +126,7 @@ pub struct Grid { width: i64, } -impl Grid { +impl Grid { pub fn new(width: i64) -> Self { Self { data: Vec::new(), @@ -134,7 +134,7 @@ impl Grid { } } /// Returns a new [Grid] with the same shape (width x height) as `self`, filled with `fill` - pub fn same_shape(&self, fill: NT) -> Grid { + pub fn same_shape(&self, fill: NT) -> Grid { Grid { data: Vec::from_iter(repeat(fill).take(self.width() * self.height())), width: self.width,