diff --git a/src/day16.rs b/src/day16.rs new file mode 100644 index 0000000..eb41665 --- /dev/null +++ b/src/day16.rs @@ -0,0 +1,183 @@ +use crate::util::maps::*; +use itertools::Itertools; +use std::collections::HashMap; + +pub fn day16() { + let input: Map = Map::from_string(&crate::input(16)); + let maze: Map = input.map(|&c| c == '.' || c == 'S' || c == 'E'); + + let start: Transform = input + .enumerate() + .find(|(_, _, &c)| c == 'S') + .map(|(x, y, _)| Transform::new(x, y, Direction::East, &maze)) + .unwrap(); + let end: (usize, usize) = input + .enumerate() + .find(|(_, _, &c)| c == 'E') + .map(|(x, y, _)| (x, y)) + .unwrap(); + + let (cheapest_cost, paths) = cheapest_paths(start, end); + + let tiles_crossed: usize = paths + .iter() + .flatten() + .sorted_by_key(|p| (p.x, p.y)) + .dedup_by(|a, b| a.x == b.x && a.y == b.y) + .count(); + + println!("Found {} shortest paths.", paths.len()); + println!("Cheapest Path to End: {}", cheapest_cost); + println!("Tiles Crossed: {}", tiles_crossed); +} + +fn cheapest_paths(start: Transform, end: (usize, usize)) -> (u64, Vec>) { + let paths: Vec<(Vec, u64)> = all_paths(start, end) + .into_iter() + // path -> (path, cost) + .map(|path| { + let cost = path.windows(2).into_iter().fold(0, |acc, w| { + acc + if w[0].direction == w[1].direction { + 1 + } else { + 1000 + } + }); + (path, cost) + }) + // keep only the paths tied for cheapest + .min_set_by_key(|(_, cost)| *cost); + let cheapest_cost = paths[0].1; + // (path, cost) -> path + let paths = paths.into_iter().map(|(path, _)| path).collect(); + (cheapest_cost, paths) +} + +fn all_paths(start: Transform, end: (usize, usize)) -> Vec> { + let costs = maze_costs(start); + + fn collect_paths<'a, 'b>( + costs: &'b HashMap, u64>, + p: Transform<'a>, + ) -> Vec>> { + let cost_p = costs[&p]; + + // cost 0 means this is the start + if cost_p == 0 { + return vec![vec![p]]; + }; + // otherwise we look at all ways that we might get to p + p.comes_from() + .into_iter() + // no cost stored means that x is a silly place to ever come from (a dead end) + .filter(|x| costs.contains_key(x)) + // if cost_x < cost_p, x most be closer to the start than p is + // otherwise, we don't want to check x (that would be anti-progress) + .filter(|x| costs[x] < cost_p) + .flat_map(|x| { + // now we recur, getting all paths from start to x + let mut paths = collect_paths(costs, x); + // and add ourself to the paths, then send the paths up the chain + paths.iter_mut().for_each(|path| path.push(p)); + paths + }) + .collect() + } + + // every facing direction at the end may have a different cost + let endpoints = costs.keys().filter(|t| t.x == end.0 && t.y == end.1); + endpoints + .into_iter() + .flat_map(|&endpoint| collect_paths(&costs, endpoint)) + .collect() +} + +fn maze_costs(start: Transform) -> HashMap { + fn calculate_costs<'a, 'b>(costs: &'b mut HashMap, u64>, p: Transform<'a>) { + // cost to any x adjacent to p is the cost to get to p + the cost to travel p -> x + // and if that's cheaper than any previous way we've found, repeat for each x as the new p + for (travel_cost, adjacent) in p.goes_to() { + let total_cost = costs[&p] + travel_cost; + let existing_cost = costs.get(&adjacent).copied().unwrap_or(u64::MAX); + if total_cost < existing_cost { + costs.insert(adjacent, total_cost); + calculate_costs(costs, adjacent); + } + } + } + + let mut costs = HashMap::new(); + costs.insert(start, 0); + calculate_costs(&mut costs, start); + costs +} + +#[derive(Copy, Clone)] +struct Transform<'a> { + x: usize, + y: usize, + direction: Direction, + map: &'a Map, +} + +impl<'a> Transform<'a> { + fn new(x: usize, y: usize, direction: Direction, map: &'a Map) -> Self { + Self { + x, + y, + direction, + map, + } + } + + fn goes_to(self) -> Vec<(u64, Transform<'a>)> { + let mut ret: Vec<(u64, Transform)> = vec![ + self.direction.clockwise(), + self.direction.counterclockwise(), + ] + .into_iter() + .filter(|d| self.map.travel_get(self.x, self.y, *d) == Some(true)) + .map(|d| (1000, Self::new(self.x, self.y, d, self.map))) + .collect(); + if let Some((nx, ny, true)) = self.map.travel_and_get(self.x, self.y, self.direction) { + ret.push((1, Self::new(nx, ny, self.direction, self.map))); + } + ret + } + + fn comes_from(self) -> Vec> { + let mut ret: Vec> = vec![ + self.direction.clockwise(), + self.direction.counterclockwise(), + ] + .into_iter() + .map(|d| Self::new(self.x, self.y, d, self.map)) + .collect(); + let backward = self + .map + .travel_and_get(self.x, self.y, self.direction.opposite()); + if let Some((nx, ny, true)) = backward { + ret.push(Self::new(nx, ny, self.direction, self.map)); + } + ret + } +} + +impl<'a> PartialEq for Transform<'a> { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.map, other.map) + && self.x == other.x + && self.y == other.y + && self.direction == other.direction + } +} + +impl<'a> Eq for Transform<'a> {} + +impl<'a> std::hash::Hash for Transform<'a> { + fn hash(&self, state: &mut H) { + self.x.hash(state); + self.y.hash(state); + self.direction.hash(state); + } +} diff --git a/src/main.rs b/src/main.rs index 8f686d5..ce7a2f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,10 +28,12 @@ mod day12; mod day13; #[allow(dead_code)] mod day14; +#[allow(dead_code)] mod day15; +mod day16; fn main() { - day15::day15(); + day16::day16(); } pub fn input(day: u8) -> String { diff --git a/src/util/maps.rs b/src/util/maps.rs index 97e4d9c..a5af1bf 100644 --- a/src/util/maps.rs +++ b/src/util/maps.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use itertools::Itertools; -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum Direction { North, South, @@ -27,6 +27,24 @@ impl Direction { pub fn is_parallel(self, other: Self) -> bool { self == other || self == other.opposite() } + + pub fn clockwise(self) -> Self { + match self { + Self::North => Self::East, + Self::East => Self::South, + Self::South => Self::West, + Self::West => Self::North, + } + } + + pub fn counterclockwise(self) -> Self { + match self { + Self::North => Self::West, + Self::West => Self::South, + Self::South => Self::East, + Self::East => Self::North, + } + } } #[derive(Clone)] @@ -134,6 +152,11 @@ where pub fn enumerate(&self) -> impl Iterator { self.coordinates().map(|(x, y)| (x, y, &self.map[y][x])) } + + pub fn map U>(&self, f: F) -> Map { + let v = self.map.iter().map(|row| row.iter().map(|e| f(e)).collect()).collect(); + Map::::from_2d_vec(v) + } } impl Display for Map