mirror of
https://github.com/bertptrs/adventofcode.git
synced 2025-12-25 21:00:31 +01:00
215 lines
6.0 KiB
Rust
215 lines
6.0 KiB
Rust
use std::collections::VecDeque;
|
|
use std::mem;
|
|
|
|
use ahash::AHashSet;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use strength_reduce::StrengthReducedUsize;
|
|
|
|
fn gcd(mut a: usize, mut b: usize) -> usize {
|
|
while a % b != 0 {
|
|
b = a % b;
|
|
mem::swap(&mut a, &mut b);
|
|
}
|
|
|
|
b
|
|
}
|
|
|
|
fn lcm(a: usize, b: usize) -> usize {
|
|
a * b / gcd(a, b)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Storm {
|
|
// Dimensions of the entire area. Includes the wall
|
|
width: usize,
|
|
height: usize,
|
|
|
|
// Periods of repetition. Basically the dimensions without the wall
|
|
width_period: StrengthReducedUsize,
|
|
height_period: StrengthReducedUsize,
|
|
combined_period: StrengthReducedUsize,
|
|
|
|
// Flying blizzards by direction and starting point
|
|
left_right: AHashSet<(usize, usize)>,
|
|
right_left: AHashSet<(usize, usize)>,
|
|
top_bottom: AHashSet<(usize, usize)>,
|
|
bottom_top: AHashSet<(usize, usize)>,
|
|
}
|
|
|
|
impl Storm {
|
|
/// Whether you can stand in the given position at the given time
|
|
fn can_stand(&self, time: usize, (x, y): (usize, usize)) -> bool {
|
|
!self
|
|
.right_left
|
|
.contains(&((x + time) % self.width_period, y))
|
|
&& !self
|
|
.bottom_top
|
|
.contains(&(x, (y + time) % self.height_period))
|
|
&& !self.left_right.contains(&(
|
|
(self.width_period.get() - time % self.width_period + x) % self.width_period,
|
|
y,
|
|
))
|
|
&& !self.top_bottom.contains(&(
|
|
x,
|
|
(self.height_period.get() - time % self.height_period + y) % self.height_period,
|
|
))
|
|
}
|
|
|
|
fn dist(&self, from: (usize, usize), goal: (usize, usize), start: usize) -> Result<usize> {
|
|
let mut todo = VecDeque::new();
|
|
todo.push_back((start, from));
|
|
|
|
let mut visited = AHashSet::new();
|
|
|
|
while let Some((time, pos)) = todo.pop_front() {
|
|
let mut enqueue = |pos| {
|
|
let new_time = time + 1;
|
|
|
|
if self.can_stand(new_time, pos)
|
|
&& visited.insert((new_time % self.combined_period, pos))
|
|
{
|
|
todo.push_back((new_time, pos));
|
|
}
|
|
};
|
|
|
|
// Waiting is perhaps an option
|
|
enqueue(pos);
|
|
|
|
// If not in the starting position or the right edge
|
|
if pos.0 > 1 && pos.1 < self.height - 1 {
|
|
enqueue((pos.0 - 1, pos.1));
|
|
}
|
|
|
|
if pos.1 > 0 && pos.0 < self.width - 2 {
|
|
enqueue((pos.0 + 1, pos.1));
|
|
}
|
|
|
|
if pos.1 > 1 {
|
|
enqueue((pos.0, pos.1 - 1));
|
|
}
|
|
|
|
if pos.0 > 0 && pos.1 < self.height - 2 {
|
|
enqueue((pos.0, pos.1 + 1));
|
|
}
|
|
|
|
if pos.1 >= 1 && (pos.0, pos.1 - 1) == goal {
|
|
return Ok(time + 1);
|
|
}
|
|
|
|
if (pos.0, pos.1 + 1) == goal {
|
|
return Ok(time + 1);
|
|
}
|
|
}
|
|
|
|
anyhow::bail!("Did not find a route to {goal:?}")
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&'_ [u8]> for Storm {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: &'_ [u8]) -> Result<Self, Self::Error> {
|
|
let width = value
|
|
.iter()
|
|
.position(|&b| b == b'\n')
|
|
.context("Could not find end of line")?;
|
|
let height = value.len() / (width + 1);
|
|
|
|
let width_period = StrengthReducedUsize::new(width - 2);
|
|
let height_period = StrengthReducedUsize::new(height - 2);
|
|
let combined_period = StrengthReducedUsize::new(lcm(width - 2, height - 2));
|
|
|
|
let mut left_right = AHashSet::new();
|
|
let mut right_left = AHashSet::new();
|
|
let mut top_bottom = AHashSet::new();
|
|
let mut bottom_top = AHashSet::new();
|
|
|
|
for (y, line) in value
|
|
.split(|&b| b == b'\n')
|
|
.enumerate()
|
|
.skip(1)
|
|
.take(height - 2)
|
|
{
|
|
for (x, &c) in line.iter().enumerate() {
|
|
match c {
|
|
b'>' => left_right.insert((x % width_period, y)),
|
|
b'<' => right_left.insert((x % width_period, y)),
|
|
b'v' => top_bottom.insert((x, y % height_period)),
|
|
b'^' => bottom_top.insert((x, y % height_period)),
|
|
_ => continue,
|
|
};
|
|
}
|
|
}
|
|
|
|
Ok(Storm {
|
|
width,
|
|
height,
|
|
width_period,
|
|
height_period,
|
|
combined_period,
|
|
left_right,
|
|
right_left,
|
|
top_bottom,
|
|
bottom_top,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn part1(input: &[u8]) -> Result<String> {
|
|
let storm = Storm::try_from(input)?;
|
|
let goal = (storm.width - 2, storm.height - 1);
|
|
|
|
storm.dist((1, 0), goal, 0).map(|d| d.to_string())
|
|
}
|
|
|
|
pub fn part2(input: &[u8]) -> Result<String> {
|
|
let storm = Storm::try_from(input)?;
|
|
let goal = (storm.width - 2, storm.height - 1);
|
|
|
|
let there = storm.dist((1, 0), goal, 0)?;
|
|
let back_again = storm.dist(goal, (1, 0), there)?;
|
|
|
|
storm.dist((1, 0), goal, back_again).map(|s| s.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const SAMPLE: &[u8] = include_bytes!("./samples/24.txt");
|
|
|
|
#[test]
|
|
fn test_can_stand() {
|
|
let storm = Storm::try_from(SAMPLE).unwrap();
|
|
dbg!(&storm);
|
|
|
|
// Test a storm moving right to left
|
|
assert!(storm.can_stand(0, (4, 2)));
|
|
assert!(!storm.can_stand(1, (4, 2)));
|
|
assert!(!storm.can_stand(0, (6, 2)));
|
|
assert!(storm.can_stand(1, (6, 2)));
|
|
|
|
// Test a storm moving bottom to top
|
|
assert!(!storm.can_stand(0, (4, 4)));
|
|
assert!(storm.can_stand(1, (4, 4)));
|
|
|
|
// Simple moving to the right
|
|
assert!(!storm.can_stand(0, (1, 1)));
|
|
assert!(storm.can_stand(1, (1, 1)));
|
|
|
|
assert!(storm.can_stand(0, (1, 2)));
|
|
assert!(!storm.can_stand(1, (1, 2)));
|
|
}
|
|
|
|
#[test]
|
|
fn sample_part1() {
|
|
assert_eq!(part1(SAMPLE).unwrap(), "18");
|
|
}
|
|
|
|
#[test]
|
|
fn sample_part2() {
|
|
assert_eq!(part2(SAMPLE).unwrap(), "54");
|
|
}
|
|
}
|