diff --git a/2022/day12-part2/Cargo.toml b/2022/day12-part2/Cargo.toml new file mode 100644 index 0000000..9fa9653 --- /dev/null +++ b/2022/day12-part2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day12-part2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/2022/day12-part2/src/main.rs b/2022/day12-part2/src/main.rs new file mode 100644 index 0000000..908ca3e --- /dev/null +++ b/2022/day12-part2/src/main.rs @@ -0,0 +1,275 @@ +use std::{cell::RefCell, cmp::Ordering, collections::BinaryHeap, rc::Rc}; + +struct Node { + x: usize, + y: usize, + height: u8, + outgoing: Vec, + best_dist: usize, + previous: Option, +} + +// Make it easier to refer to Node references. +type NodeRef = Rc>; + +impl Node { + fn new(x: usize, y: usize, height: u8) -> NodeRef { + Rc::new(RefCell::new(Node { + x, + y, + height, + outgoing: Vec::new(), + best_dist: usize::MAX, + previous: None, + })) + } + + fn heuristic(&self, dx: usize, dy: usize) -> usize { + // Manhattan distance + let x_dist = ((self.x as isize) - (dx as isize)).unsigned_abs(); + let y_dist = ((self.y as isize) - (dy as isize)).unsigned_abs(); + x_dist + y_dist + } + + fn to_visit_node(&self, best_est: usize) -> VisitNode { + VisitNode { + x: self.x, + y: self.y, + best_est, + } + } +} + +// PartialEq can be implemented automatically. +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +struct VisitNode { + y: usize, + x: usize, + best_est: usize, +} + +// I've opted to go with the coordinates instead of pointers +// to make it easier to achieve the total order. + +// Adapted from the Rust docs on priority queues: +// https://doc.rust-lang.org/std/collections/binary_heap/index.html + +// Manually implement Ord for VisitNode to ensure the queue becomes a min-heap. +// However, for y and x we keep the normal order. +impl Ord for VisitNode { + fn cmp(&self, other: &Self) -> Ordering { + // Notice the flipped order best_est here. + other.best_est.cmp(&self.best_est) + .then_with(|| self.y.cmp(&other.y)) + .then_with(|| self.x.cmp(&other.x)) + } +} + +// For PartialOrd simply use the total order. +impl PartialOrd for VisitNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn main() { + // Use command line arguments to specify the input filename. + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + panic!("Usage: ./main \nNo input file provided. Exiting."); + } + + // Next, read the contents of the input file into a string for easier processing. + let input = std::fs::read_to_string(&args[1]).expect("Error opening file"); + + // --- TASK BEGIN --- + + // Understand the size of the map we're working with. + let width = input.lines().next().unwrap().chars().count(); + let height = input.lines().count(); + + // Keep a complete map of all nodes, spatially distributed. + // We first map lines, then columns, i.e. map[y][x]. + // Origin of the coordinate system is the top right. + let mut map: Vec> = Vec::with_capacity(height); + + // Also keep references to start and destination nodes. + let mut start: Option = None; + let mut dest: Option = None; + + // Parse the map. + for (y, line) in input.lines().enumerate() { + map.push(Vec::new()); + for (x, c) in line.chars().enumerate() { + // Translate the character into the correct elevation. + let elevation: u8 = match c { + 'S' => 0, + 'E' => 25, + a => (a as u8) - b'a', + }; + + // Create the node. + let node = Node::new(x, y, elevation); + + // Store starting and destination node. + if c == 'S' { + start = Some(node.clone()); + } else if c == 'E' { + dest = Some(node.clone()); + } + + // Store the node in the global map. + map.last_mut().unwrap().push(node.clone()); + } + } + + // Store the coordinates of the destination for the heuristics later. + let dest_x = dest.as_ref().unwrap().borrow().x; + let dest_y = dest.as_ref().unwrap().borrow().y; + + // Create the neighbor relationship for all nodes, where applicable. + for (y, line) in input.lines().enumerate() { + for (x, _) in line.chars().enumerate() { + // Grab a counted reference to the cell we're currently looking at. + let mut current_node = map[y][x].borrow_mut(); + + // Only create the following neighbor relationships if the heights allow it. + + // NORTH + if y > 0 { + let other_node = map[y - 1][x].clone(); + // Instead of at most one step of elevation (part one) now check + // for at most one step down between adjacent tiles. + if current_node.height <= other_node.borrow().height + 1 { + current_node.outgoing.push(other_node); + } + } + + // SOUTH + if y < height - 1 { + let other_node = map[y + 1][x].clone(); + // Instead of at most one step of elevation (part one) now check + // for at most one step down between adjacent tiles. + if current_node.height <= other_node.borrow().height + 1 { + current_node.outgoing.push(other_node); + } + } + + // WEST + if x > 0 { + let other_node = map[y][x - 1].clone(); + // Instead of at most one step of elevation (part one) now check + // for at most one step down between adjacent tiles. + if current_node.height <= other_node.borrow().height + 1 { + current_node.outgoing.push(other_node); + } + } + + // EAST + if x < width - 1 { + let other_node = map[y][x + 1].clone(); + // Instead of at most one step of elevation (part one) now check + // for at most one step down between adjacent tiles. + if current_node.height <= other_node.borrow().height + 1 { + current_node.outgoing.push(other_node); + } + } + } + } + + // Keep track of all nodes that need to be visited still. + // We're going to use an efficient priority queue for this. + let mut to_visit: BinaryHeap = BinaryHeap::new(); + + // Add the destionation node (E) to that queue. (part two) + let mut start_node = dest.as_ref().unwrap().borrow_mut(); + // Set 0 as the current best distance. + to_visit.push(start_node.to_visit_node(0)); + start_node.best_dist = 0; + drop(start_node); + + // For the visualization. + let mut solution_node: Option = None; + + // Finally, actually start the A* path finding algorithm. + while let Some(current_vn) = to_visit.pop() { + // Grab a mutable borrow to the actual node. + let current_node = map[current_vn.y][current_vn.x].borrow_mut(); + + // Is this node on elevation level 0 a.k.a. 'a'? + if current_node.height == 0 { + // We're done here! + println!("Smallest distance from 'E' to 'a': {}", current_node.best_dist); + solution_node = Some(map[current_vn.y][current_vn.x].clone()); + break; + } + + // Now iterate through all neighbors. + for nb in ¤t_node.outgoing { + // Grab a mutable borrow to that neighbor. + let mut nb = nb.borrow_mut(); + + // Calculate the best known distance. + let actual_dist = current_node.best_dist + 1; + + // Check if the computed distance is better than the previous optimum. + if actual_dist < nb.best_dist { + // Nice! + // Update its distance. + nb.best_dist = actual_dist; + // Update its predecessor (point to us). + nb.previous = Some(map[current_vn.y][current_vn.x].clone()); + // Add it to the priority queue. + // PART TWO - Simply ignore the heuristic and let the algorithm + // degenerate to Dijkstra's shortest path. + // let heuristic = nb.heuristic(dest_x, dest_y); + let heuristic = 0; + to_visit.push(nb.to_visit_node(actual_dist + heuristic)); + } + } + } + + print_solution(&map, solution_node.clone().unwrap()); + + // println!( + // "Minimal distance on destination: {}", + // dest.as_ref().unwrap().borrow().best_dist + // ); +} + +fn print_solution(map: &Vec>, dest: NodeRef) { + // Store the output. + let mut output: Vec> = Vec::new(); + + // Recreate the input. + for line in map { + output.push(Vec::new()); + for nr in line { + // Grab a borrow to the actual node. + let node = nr.borrow(); + // Convert the height back into a character, lol. + output.last_mut().unwrap().push((b'a' + node.height) as char); + } + } + + // Retrace the optimal path and replace the letters with arrows. + let mut cn = dest; + loop { + // Overwrite the character with a #. + output[cn.borrow().y][cn.borrow().x] = '#'; + if cn.borrow().previous.is_some() { + let prev = cn.borrow().previous.clone().unwrap(); + cn = prev; + } else { + break; + } + } + + // Actually print. + for line in output { + for char in line { + print!("{}", char); + } + println!(); + } +}