adventofcode2023/day17/src/main_visualization.rs
2023-12-17 19:06:47 +01:00

166 lines
7.0 KiB
Rust

use std::collections::BTreeMap;
use std::fs::read_to_string;
use std::time::Instant;
fn find_path(layout: &[&[u8]], min_consecutive: u8, max_consecutive: u8,
out_path: &mut Vec<(i32, i32)>, out_history: &mut Vec<(i32, i32)>) -> i32 {
#[derive(Copy, Clone, PartialEq)]
#[repr(u8)]
enum Direction {
Left = 0,
Right = 1,
Up = 2,
Down = 3,
}
const ALL_DIRECTIONS: [Direction; 4] = [Direction::Left, Direction::Right, Direction::Up, Direction::Down];
const OPPOSITE_DIRECTION: [Direction; 4] = [Direction::Right, Direction::Left, Direction::Down, Direction::Up];
const DIRECTION_OFFSET: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
struct Node {
x: i32,
y: i32,
cost: i32,
dir: Direction,
consecutive: u8,
}
let width = layout[0].len();
let height = layout.len();
let index_multiplier = max_consecutive as usize * 4;
// Evaluation score for visited nodes, -1 is unvisited
// Node is treated as a different one unless direction and consecutive counter are the same
let mut scores: Vec<i32> = vec![-1; width * height * index_multiplier];
let mut previous: Vec<i32> = vec![-1; width * height * index_multiplier];
let mut queue: BTreeMap<i32, Node> = BTreeMap::new();
let get_node_index = |node: &Node| -> usize {
(node.y as usize * width + node.x as usize) * index_multiplier
+ node.dir as usize * max_consecutive as usize + node.consecutive as usize - 1
};
let target_x = width as i32 - 1;
let target_y = height as i32 - 1;
let get_heuristic = |node: &Node| -> i32 {
// Heuristic is manhattan distance with addition of correction due to forced turns
// This correction seems to give minor but nonzero improvement of performance
let dx = target_x - node.x;
let dy = target_y - node.y;
let x_correction = dx / max_consecutive as i32 * min_consecutive as i32;
let y_correction = dy / max_consecutive as i32 * min_consecutive as i32;
dx + x_correction + dy + y_correction
};
let mut current_node = Node { x: 0, y: 0, cost: 0, dir: Direction::Right, consecutive: 0 };
let mut current_index: i32 = -1;
'outer: loop {
out_history.push((current_node.x, current_node.y));
for dir in ALL_DIRECTIONS {
// Can't turn 180
if OPPOSITE_DIRECTION[current_node.dir as usize] == dir {
continue;
}
// Can't go in one direction less than min_consecutive tiles, 0 is only for start
if current_node.consecutive > 0 && current_node.consecutive < min_consecutive
&& current_node.dir != dir {
continue;
}
// Compute new node consecutive counter
let mut new_node = Node {
x: current_node.x,
y: current_node.y,
cost: current_node.cost,
dir: dir,
consecutive: if current_node.dir == dir { current_node.consecutive + 1 } else { 1 },
};
// Can't go in one direction more than max_consecutive tiles
if new_node.consecutive > max_consecutive as u8 {
continue;
}
// Calculating new node position and bounds checking
let (offset_x, offset_y) = DIRECTION_OFFSET[dir as usize];
new_node.x += offset_x;
new_node.y += offset_y;
if new_node.x < 0 || new_node.y < 0 || new_node.x as usize >= width || new_node.y as usize >= height {
continue;
}
// If the node hasn't been visited yet
let node_index = get_node_index(&new_node);
if scores[node_index] < 0 {
let new_tile_cost = (layout[new_node.y as usize][new_node.x as usize] - b'0') as i32;
new_node.cost += new_tile_cost;
let new_score = new_node.cost + get_heuristic(&new_node);
scores[node_index] = new_score;
previous[node_index] = current_index;
if new_node.x == target_x && new_node.y == target_y && new_node.consecutive >= min_consecutive {
current_node = new_node;
break 'outer;
}
// Combines node score with node index to provide unique keys
queue.insert((new_score as usize * scores.len() + node_index) as i32, new_node);
}
}
match queue.pop_first() {
Some((_, node)) => {
current_node = node;
current_index = get_node_index(&current_node) as i32;
}
None => { return -1; }
}
}
let mut temp_x = current_node.x;
let mut temp_y = current_node.y;
let mut temp_index = get_node_index(&current_node) as i32;
while temp_index >= 0 {
out_path.push((temp_x, temp_y));
temp_index = previous[temp_index as usize];
temp_x = temp_index / index_multiplier as i32;
temp_y = temp_x / width as i32;
temp_x -= temp_y * width as i32;
}
current_node.cost
}
fn visualize_path(layout: &[&[u8]], path: &Vec<(i32, i32)>) {
let mut layout_copy = layout.iter().map(|&bytes| Vec::from(bytes)).collect::<Vec<_>>();
for &(x, y) in path {
layout_copy[y as usize][x as usize] = b'.';
}
for line in layout_copy.iter() {
println!("{}", std::str::from_utf8(&line).unwrap());
}
}
fn visualize_history(layout: &[&[u8]], history: &Vec<(i32, i32)>) {
let grayscale = " .:-=+*#%@%#*+=-:. ".as_bytes();
let mut layout_history = layout.iter().map(|&bytes| Vec::from(bytes)).collect::<Vec<_>>();
for (i, &(x, y)) in history.iter().enumerate() {
layout_history[y as usize][x as usize] = grayscale[i * grayscale.len() / history.len()];
}
for line in layout_history.iter() {
println!("{}", std::str::from_utf8(&line).unwrap());
}
}
fn main() {
let time_start = Instant::now();
let input_str = read_to_string("input.txt").unwrap();
let time_start_no_io = Instant::now();
let layout = input_str.lines().map(|str| str.as_bytes()).collect::<Vec<_>>();
let mut path: Vec<(i32, i32)> = vec![];
let mut history: Vec<(i32, i32)> = vec![];
let mut path2: Vec<(i32, i32)> = vec![];
let mut history2: Vec<(i32, i32)> = vec![];
let loss1 = find_path(&layout, 0, 3, &mut path, &mut history);
let loss2 = find_path(&layout, 4, 10, &mut path2, &mut history2);
let elapsed = time_start.elapsed().as_micros();
let elapsed_no_io = time_start_no_io.elapsed().as_micros();
println!("Part 1 path:");
visualize_path(&layout, &path);
println!("\nPart 1 search order:");
visualize_history(&layout, &history);
println!("\nPart 2 path:");
visualize_path(&layout, &path2);
println!("\nPart 2 search order:");
visualize_history(&layout, &history2);
println!("Time: {}us", elapsed);
println!("Time without file i/o: {}us", elapsed_no_io);
println!("Loss1: {}", loss1);
println!("Loss2: {}", loss2);
}