aoc/year2024/day16.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
//! # Reindeer Maze
//!
//! Solves part one and part two simultaneously.
//!
//! Part one is a normal [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)
//! search from start to end.
//!
//! Part two is a a BFS *backwards* from the end to the finish, tracing the cost exactly
//! to find all possible paths. This reuses the cost information from the Dijkstra without
//! requiring any extra state keeping for the paths.
use crate::util::grid::*;
use crate::util::point::*;
use std::collections::VecDeque;
type Input = (i32, usize);
/// Clockwise order starting with facing right.
const DIRECTIONS: [Point; 4] = [RIGHT, DOWN, LEFT, UP];
pub fn parse(input: &str) -> Input {
let grid = Grid::parse(input);
let start = grid.find(b'S').unwrap();
let end = grid.find(b'E').unwrap();
// Forwards Dijkstra. Since turns are so much more expensive than moving forward, we can
// treat this as a glorified BFS using two priority queues. This is much faster than using
// an actual min heap.
let mut todo_first = VecDeque::new();
let mut todo_second = VecDeque::new();
// State is `(position, direction)`.
let mut seen = grid.same_size_with([i32::MAX; 4]);
let mut lowest = i32::MAX;
todo_first.push_back((start, 0, 0));
seen[start][0] = 0;
while !todo_first.is_empty() {
while let Some((position, direction, cost)) = todo_first.pop_front() {
if cost >= lowest {
continue;
}
if position == end {
lowest = cost;
continue;
}
// -1.rem_euclid(4) = 3
let left = (direction + 3) % 4;
let right = (direction + 1) % 4;
let next = [
(position + DIRECTIONS[direction], direction, cost + 1),
(position, left, cost + 1000),
(position, right, cost + 1000),
];
for tuple @ (next_position, next_direction, next_cost) in next {
if grid[next_position] != b'#' && next_cost < seen[next_position][next_direction] {
// Find the next bucket.
if next_direction == direction {
todo_first.push_back(tuple);
} else {
todo_second.push_back(tuple);
}
seen[next_position][next_direction] = next_cost;
}
}
}
(todo_first, todo_second) = (todo_second, todo_first);
}
// Backwards BFS
let mut todo = VecDeque::new();
let mut path = grid.same_size_with(false);
// Lowest paths can arrive at end node in multiple directions.
for direction in 0..4 {
if seen[end][direction] == lowest {
todo.push_back((end, direction, lowest));
}
}
while let Some((position, direction, cost)) = todo.pop_front() {
path[position] = true;
if position == start {
continue;
}
// Reverse direction and subtract cost.
let left = (direction + 3) % 4;
let right = (direction + 1) % 4;
let next = [
(position - DIRECTIONS[direction], direction, cost - 1),
(position, left, cost - 1000),
(position, right, cost - 1000),
];
for (next_position, next_direction, next_cost) in next {
// Trace our cost step by step so it will exactly match possible paths.
if next_cost == seen[next_position][next_direction] {
todo.push_back((next_position, next_direction, next_cost));
// Set cost back to `i32::MAX` to prevent redundant path explorations.
seen[next_position][next_direction] = i32::MAX;
}
}
}
(lowest, path.bytes.iter().filter(|&&b| b).count())
}
pub fn part1(input: &Input) -> i32 {
input.0
}
pub fn part2(input: &Input) -> usize {
input.1
}