Solution for 2022/day12-part2
This commit is contained in:
parent
c78ea1c040
commit
5928c3fbb3
8
2022/day12-part2/Cargo.toml
Normal file
8
2022/day12-part2/Cargo.toml
Normal file
@ -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]
|
||||||
275
2022/day12-part2/src/main.rs
Normal file
275
2022/day12-part2/src/main.rs
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
use std::{cell::RefCell, cmp::Ordering, collections::BinaryHeap, rc::Rc};
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
height: u8,
|
||||||
|
outgoing: Vec<NodeRef>,
|
||||||
|
best_dist: usize,
|
||||||
|
previous: Option<NodeRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it easier to refer to Node references.
|
||||||
|
type NodeRef = Rc<RefCell<Node>>;
|
||||||
|
|
||||||
|
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<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Use command line arguments to specify the input filename.
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
panic!("Usage: ./main <input-file>\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<NodeRef>> = Vec::with_capacity(height);
|
||||||
|
|
||||||
|
// Also keep references to start and destination nodes.
|
||||||
|
let mut start: Option<NodeRef> = None;
|
||||||
|
let mut dest: Option<NodeRef> = 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<VisitNode> = 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<NodeRef> = 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<Vec<NodeRef>>, dest: NodeRef) {
|
||||||
|
// Store the output.
|
||||||
|
let mut output: Vec<Vec<char>> = 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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user