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 { 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 { 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 { 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 { 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"); } }