diff --git a/day16/Cargo.toml b/day16/Cargo.toml new file mode 100644 index 0000000..b2016c1 --- /dev/null +++ b/day16/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day16" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/day16/src/main.rs b/day16/src/main.rs new file mode 100644 index 0000000..399c53a --- /dev/null +++ b/day16/src/main.rs @@ -0,0 +1,249 @@ +use std::cmp::Ordering; +use std::collections::HashMap; +use std::collections::HashSet; +use std::collections::BinaryHeap; + +use std::env; +use std::fs; + +use crate::Dir::{North, South, East, West }; + +type Coord = (i32, i32); +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] +enum Dir { North, South, East, West } + +#[derive(Copy, Clone, Eq, PartialEq)] +struct State { + cost: i32, + position: Coord, + facing: Dir, +} + +impl Ord for State { + fn cmp(&self, other: &Self) -> Ordering { + // flipped ordering on cost, so that it's a min-heap and not a max-heap + other.cost.cmp(&self.cost) + .then_with(|| self.position.cmp(&other.position)) + .then_with(|| self.facing.cmp(&other.facing)) + } +} + +// `PartialOrd` needs to be implemented as well. +impl PartialOrd for State { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn step(start: &Coord, d: Dir, steps: i32) -> Coord { + match d { + North => (start.0, start.1 - steps), + South => (start.0, start.1 + steps), + East => (start.0 + steps, start.1), + West => (start.0 - steps, start.1), + } +} + +fn main() { + println!("Hello, AoC day 13!"); + + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("wrong number of arguments!"); + std::process::exit(1); + } + + let file_path = &args[1]; + + let file = fs::read_to_string(file_path) + .expect("should be able to read the file"); + + let mut map = HashMap::new(); + + let mut start = (0,0); + let mut exit = (0,0); + let mut y = 0; + for line in file.lines() { + let mut x = 0; + for c in line.chars() { + match c { + 'S' => { start = (x,y); map.insert((x,y), '.'); }, + 'E' => { exit = (x,y); map.insert((x,y), '.'); }, + _ => { map.insert((x,y), c); } + }; + x += 1; + } + y += 1; + } + + let score = solve_maze(&map, start, exit); + + println!("the score was {score:?}"); + + let distances = assign_distances(&map, start); + println!("found {} distances", distances.len()); + + let paths = find_paths(&map, &distances, start, exit); + + let mut touched_squares: HashSet = HashSet::new(); + + for path in paths { + for (location, _facing) in path { + touched_squares.insert(location); + } + } + + println!("there were {} squares on the best paths", touched_squares.len()); + +} + +fn solve_maze(map: &HashMap, start: Coord, exit: Coord) -> Option { + let mut heap = BinaryHeap::new(); + let mut distances: HashMap<(Coord, Dir), i32> = HashMap::new(); + + heap.push(State { position: start, facing: East, cost: 0}); + + while let Some(State { position, facing, cost}) = heap.pop() { + if position == exit { return Some(cost); } + + if let Some(previous_cost) = distances.get(&(position, facing)) { + if *previous_cost < cost { continue; } + } + + distances.insert((position, facing), cost); + + for new_dir in [North, South, East, West] { + let new_spot = step(&position, new_dir, 1); + let mut new_cost = cost + 1; + if facing != new_dir { new_cost = new_cost + 1000} + + if let Some(d) = distances.get(&(new_spot, new_dir)) { + if *d < new_cost { continue; } + } + + if map.get(&new_spot) == Some(&'.') { + heap.push(State { + position: new_spot, + facing: new_dir, + cost: new_cost + }); + } + } + } + + return None; +} + + +fn assign_distances(map: &HashMap, start: Coord) -> HashMap<(Coord, Dir), i32> { + let mut heap = BinaryHeap::new(); + let mut distances = HashMap::new(); + + heap.push(State { position: start, facing: East, cost: 0}); + + while let Some(State { position, facing, cost}) = heap.pop() { + + if let Some(previous_cost) = distances.get(&(position, facing)) { + if *previous_cost < cost { continue; } + } + + distances.insert((position, facing), cost); + + // option to rotate in place! very expensive + for new_dir in [North, South, East, West] { + let new_cost = cost + 1000; + if let Some(d) = distances.get(&(position, new_dir)) { + if *d < new_cost { continue; } + } + heap.push(State { + position: position, + facing: new_dir, + cost: new_cost + }); + } + + let forward_position = step(&position, facing, 1); + if Some(&'.') == map.get(&forward_position) { + let new_cost = cost + 1; + if let Some(d) = distances.get(&(forward_position, facing)) { + if *d < new_cost { continue; } + } + heap.push(State { + position: forward_position, + facing: facing, + cost: new_cost, + }) + } + } + + return distances; +} + +fn find_paths( + map: &HashMap, + distances: &HashMap<(Coord, Dir), i32>, + start: Coord, + end: Coord +) -> Vec> { + let mut paths: Vec> = Vec::new(); + let mut output: Vec> = Vec::new(); + + let mut shortest_end_distance = distances.get(&(end, North)).expect("the end has to be reachable"); + for dir in [North, South, East, West] { + if let Some(d) = distances.get(&(end, dir)) { + if d < shortest_end_distance { + shortest_end_distance = d; + } + } + } + + for dir in [North, South, East, West] { + if let Some(d) = distances.get(&(end, dir)) { + if d == shortest_end_distance { + paths.push(vec![(end, dir)]); + } + } + } + + while let Some(mut path) = paths.pop() { + // iterate through paths that lead to the end + // find all neighbors that can step to the current path head + // if the neighbor can step to the current path lead, + if let Some(head) = path.pop() { + if head == (start, East) { + path.push(head); + output.push(path); + continue; + } + let potential_predecessors = state_predecessors(map, head); + let our_distance = distances.get(&head).unwrap(); + + for state in potential_predecessors { + let candidate_distance = distances.get(&state).unwrap(); + + if candidate_distance < our_distance { + let mut this_path = path.clone(); + this_path.push(head); + this_path.push(state); + paths.push(this_path); + } + } + } + } + + return output; +} + +fn state_predecessors(map: &HashMap, end: (Coord, Dir)) -> Vec<(Coord, Dir)> { + let mut output = Vec::new(); + let walk = step(&end.0, end.1, -1); // include the possibility that we walked here. + if map.get(&walk) == Some(&'.') { output.push((walk, end.1)); } + + for old_dir in [North, South, East, West] { + if old_dir == end.1 { continue; } + + output.push((end.0, old_dir)); + } + + return output; +}