2022 Day 24 Part 1
This commit is contained in:
parent
a66e590b35
commit
26fa39cf6a
1 changed files with 258 additions and 0 deletions
258
2022/src/bin/day24.rs
Normal file
258
2022/src/bin/day24.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
use std::collections::{BinaryHeap, HashMap, HashSet};
|
||||||
|
|
||||||
|
const INPUT: &str = include_str!("../../input/day23.txt");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let valley = Valley::parse(INPUT);
|
||||||
|
println!("{}", part1(valley));
|
||||||
|
}
|
||||||
|
|
||||||
|
type RC = (usize, usize);
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum Direction {
|
||||||
|
North,
|
||||||
|
East,
|
||||||
|
South,
|
||||||
|
West,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum Entity {
|
||||||
|
Wall,
|
||||||
|
Blizzard(Direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Valley {
|
||||||
|
grid: HashMap<RC, Entity>,
|
||||||
|
start: RC,
|
||||||
|
goal: RC,
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Valley {
|
||||||
|
fn parse(text: &str) -> Self {
|
||||||
|
let mut grid: HashMap<RC, Entity> = HashMap::new();
|
||||||
|
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
|
||||||
|
for (r, line) in lines.iter().enumerate() {
|
||||||
|
for (c, fill) in line.chars().enumerate() {
|
||||||
|
let entity = match fill {
|
||||||
|
'.' => continue,
|
||||||
|
'#' => Entity::Wall,
|
||||||
|
'^' => Entity::Blizzard(Direction::North),
|
||||||
|
'>' => Entity::Blizzard(Direction::East),
|
||||||
|
'v' => Entity::Blizzard(Direction::South),
|
||||||
|
'<' => Entity::Blizzard(Direction::West),
|
||||||
|
other => panic!("{:?}", other),
|
||||||
|
};
|
||||||
|
let old = grid.insert((r, c), entity);
|
||||||
|
assert!(old.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = lines.len();
|
||||||
|
let width = lines[0].len();
|
||||||
|
|
||||||
|
let mut start = (0, 0);
|
||||||
|
for (c, fill) in lines[0].chars().enumerate() {
|
||||||
|
if fill == '.' {
|
||||||
|
start = (0, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut goal = (0, 0);
|
||||||
|
for (c, fill) in lines[lines.len() - 1].chars().enumerate() {
|
||||||
|
if fill == '.' {
|
||||||
|
goal = (lines.len() - 1, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valley = Self {
|
||||||
|
grid: grid.clone(),
|
||||||
|
start,
|
||||||
|
goal,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
valley.check();
|
||||||
|
valley
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self) {
|
||||||
|
assert_eq!(self.start, (0, 1));
|
||||||
|
assert_eq!(self.goal, (self.height - 1, self.width - 2));
|
||||||
|
|
||||||
|
for (&loc @ (r, c), ent) in &self.grid {
|
||||||
|
assert!(!self.safe(0, loc), "{:?}", ent);
|
||||||
|
assert!(r < self.height);
|
||||||
|
assert!(c < self.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(self.safe(0, self.start));
|
||||||
|
assert!(self.safe(0, self.goal));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe(&self, minutes: usize, loc @ (r, c): RC) -> bool {
|
||||||
|
if r >= self.height || c >= self.width {
|
||||||
|
// Out of bounds!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is it faster to just slice the row and column out?
|
||||||
|
for (&start, &entity) in &self.grid {
|
||||||
|
match entity {
|
||||||
|
Entity::Wall => {
|
||||||
|
if loc == start {
|
||||||
|
// Can't stand here.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity::Blizzard(dir) => {
|
||||||
|
if self.blizzard_pos(minutes, start, dir) == loc {
|
||||||
|
// Oops!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No danger... for now!
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blizzard_pos(&self, minutes: usize, (r, c): RC, dir: Direction) -> RC {
|
||||||
|
// Two walls, always.
|
||||||
|
let rr = self.height - 2;
|
||||||
|
let cc = self.width - 2;
|
||||||
|
|
||||||
|
fn sub_mod(a: usize, b: usize, m: usize) -> usize {
|
||||||
|
let a: i64 = a.try_into().unwrap();
|
||||||
|
let b: i64 = b.try_into().unwrap();
|
||||||
|
let m: i64 = m.try_into().unwrap();
|
||||||
|
((((a - b) % m) + m) % m).try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_mod(a: usize, b: usize, m: usize) -> usize {
|
||||||
|
(a + b) % m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract 1 to get into a wall-less space, and then add it back to get back to
|
||||||
|
// normal.
|
||||||
|
match dir {
|
||||||
|
Direction::North => (1 + sub_mod(r - 1, minutes, rr), c),
|
||||||
|
Direction::South => (1 + add_mod(r - 1, minutes, rr), c),
|
||||||
|
|
||||||
|
Direction::West => (r, 1 + sub_mod(c - 1, minutes, cc)),
|
||||||
|
Direction::East => (r, 1 + add_mod(c - 1, minutes, cc)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct State {
|
||||||
|
loc: RC,
|
||||||
|
minutes: usize,
|
||||||
|
heuristic: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn sort_cost(&self) -> usize {
|
||||||
|
self.minutes + self.heuristic
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destinations(&self) -> Vec<RC> {
|
||||||
|
let (r, c) = self.loc;
|
||||||
|
// If the search ever tries the start state again, this has to handle it. Somehow my search
|
||||||
|
// actually gets here _twice_, even if I pre-excute the first step to get into the grid.
|
||||||
|
let mut v = vec![];
|
||||||
|
if r > 0 {
|
||||||
|
v.push((r - 1, c));
|
||||||
|
}
|
||||||
|
|
||||||
|
v.push((r, c - 1));
|
||||||
|
v.push((r, c));
|
||||||
|
v.push((r, c + 1));
|
||||||
|
|
||||||
|
v.push((r + 1, c));
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for State {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for State {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
// BinaryHeap is a **MAX-HEAP**. To get a min-heap, we run the comparisons backwards.
|
||||||
|
other.sort_cost().cmp(&self.sort_cost())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn part1(valley: Valley) -> usize {
|
||||||
|
let mut queue: BinaryHeap<State> = BinaryHeap::new();
|
||||||
|
let mut expanded: HashSet<State> = HashSet::new();
|
||||||
|
|
||||||
|
let goal = valley.goal;
|
||||||
|
|
||||||
|
let heuristic = move |loc: RC| -> usize {
|
||||||
|
let dr = goal.0 - loc.0;
|
||||||
|
let dc = goal.1 - loc.1;
|
||||||
|
dr + dc
|
||||||
|
};
|
||||||
|
|
||||||
|
let successors = move |state: &State| -> Vec<State> {
|
||||||
|
let minutes = state.minutes + 1;
|
||||||
|
|
||||||
|
state
|
||||||
|
.destinations()
|
||||||
|
.iter()
|
||||||
|
.map(|&loc| State {
|
||||||
|
loc,
|
||||||
|
minutes,
|
||||||
|
heuristic: 0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = State {
|
||||||
|
minutes: 0,
|
||||||
|
loc: valley.start,
|
||||||
|
heuristic: 0,
|
||||||
|
};
|
||||||
|
queue.push(start);
|
||||||
|
|
||||||
|
while let Some(state) = queue.pop() {
|
||||||
|
if !expanded.insert(state.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = expanded.len();
|
||||||
|
if progress % 1000 == 0 {
|
||||||
|
dbg!(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.loc == goal {
|
||||||
|
return state.minutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
for mut next in successors(&state) {
|
||||||
|
if valley.safe(next.minutes, next.loc) {
|
||||||
|
next.heuristic = heuristic(next.loc);
|
||||||
|
queue.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg!(expanded);
|
||||||
|
panic!("Caught in a blizzard 🥶")
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue