223 lines
5.8 KiB
Rust
223 lines
5.8 KiB
Rust
#![feature(test)]
|
|
|
|
use aoc::{grid::Direction, iter_ext::IterExt};
|
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
|
use rustc_hash::FxHashSet;
|
|
|
|
type Input = Vec<Vec<Cell>>;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum Cell {
|
|
Guard, // ^
|
|
Obstruction, // #
|
|
Empty, // .
|
|
}
|
|
|
|
fn setup(input: &str) -> Input {
|
|
input
|
|
.trim()
|
|
.lines()
|
|
.map(|line| {
|
|
line.trim()
|
|
.bytes()
|
|
.map(|b| match b {
|
|
b'^' => Cell::Guard,
|
|
b'#' => Cell::Obstruction,
|
|
b'.' => Cell::Empty,
|
|
_ => panic!(),
|
|
})
|
|
.collect()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Walk<'a> {
|
|
x: usize,
|
|
y: usize,
|
|
d: Direction,
|
|
grid: &'a Input,
|
|
init: bool,
|
|
}
|
|
|
|
impl Walk<'_> {
|
|
fn from_input(input: &Input) -> Walk {
|
|
let (x, y) = input
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(y, row)| row.iter().index(&Cell::Guard).map(|x| (x, y)))
|
|
.unwrap();
|
|
Walk {
|
|
x,
|
|
y,
|
|
d: Direction::North,
|
|
grid: input,
|
|
init: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Iterator for Walk<'_> {
|
|
type Item = (usize, usize, Direction);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if !self.init {
|
|
self.init = true;
|
|
return Some((self.x, self.y, self.d));
|
|
}
|
|
|
|
loop {
|
|
let (nx, ny) = self
|
|
.d
|
|
.step(self.x, self.y, self.grid[0].len(), self.grid.len())?;
|
|
if !matches!(self.grid[ny][nx], Cell::Obstruction) {
|
|
(self.x, self.y) = (nx, ny);
|
|
break;
|
|
}
|
|
self.d = self.d.rotate_right();
|
|
}
|
|
|
|
Some((self.x, self.y, self.d))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Grid {
|
|
width: usize,
|
|
height: usize,
|
|
start: (usize, usize),
|
|
north: Vec<LookupRow>,
|
|
east: Vec<LookupRow>,
|
|
south: Vec<LookupRow>,
|
|
west: Vec<LookupRow>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LookupRow(Vec<usize>);
|
|
|
|
impl LookupRow {
|
|
fn lookup(&self, n: usize, extra: Option<usize>) -> usize {
|
|
match extra {
|
|
Some(x) if x <= n => self.0[n].min(n - x),
|
|
_ => self.0[n],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromIterator<bool> for LookupRow {
|
|
fn from_iter<T: IntoIterator<Item = bool>>(iter: T) -> Self {
|
|
Self(
|
|
iter.into_iter()
|
|
.scan(usize::MAX, |s, x| {
|
|
*s = if x { 0 } else { s.saturating_add(1) };
|
|
Some(*s)
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Grid {
|
|
fn from_input(input: &Input) -> Self {
|
|
let width = input[0].len();
|
|
let height = input.len();
|
|
|
|
let start = input
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(y, row)| row.iter().index(&Cell::Guard).map(|x| (x, y)))
|
|
.unwrap();
|
|
|
|
let is_obstruction = |x: usize, y: usize| matches!(input[y][x], Cell::Obstruction);
|
|
|
|
Self {
|
|
width,
|
|
height,
|
|
start,
|
|
north: (0..width)
|
|
.into_par_iter()
|
|
.map(|x| (0..height).map(|y| is_obstruction(x, y)).collect())
|
|
.collect(),
|
|
east: (0..height)
|
|
.into_par_iter()
|
|
.map(|y| (0..width).rev().map(|x| is_obstruction(x, y)).collect())
|
|
.collect(),
|
|
south: (0..width)
|
|
.into_par_iter()
|
|
.map(|x| (0..height).rev().map(|y| is_obstruction(x, y)).collect())
|
|
.collect(),
|
|
west: (0..height)
|
|
.into_par_iter()
|
|
.map(|y| (0..width).map(|x| is_obstruction(x, y)).collect())
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn check_cycle(&self, extra_obstruction: (usize, usize)) -> bool {
|
|
let (mut x, mut y) = self.start;
|
|
let mut d = Direction::North;
|
|
let mut visited = FxHashSet::default();
|
|
while visited.insert((x, y, d)) {
|
|
let Some((nx, ny)) = self.walk(x, y, d, extra_obstruction) else {
|
|
return false;
|
|
};
|
|
(x, y, d) = (nx, ny, d.rotate_right())
|
|
}
|
|
true
|
|
}
|
|
|
|
fn walk(
|
|
&self,
|
|
x: usize,
|
|
y: usize,
|
|
direction: Direction,
|
|
extra_obstruction: (usize, usize),
|
|
) -> Option<(usize, usize)> {
|
|
let eo = Some(extra_obstruction);
|
|
Some(match direction {
|
|
Direction::North => self.north[x].lookup(y, eo.filter(|eo| eo.0 == x).map(|eo| eo.1)),
|
|
Direction::East => self.east[y].lookup(
|
|
self.width - x - 1,
|
|
eo.filter(|eo| eo.1 == y).map(|eo| self.width - eo.0 - 1),
|
|
),
|
|
Direction::South => self.south[x].lookup(
|
|
self.height - y - 1,
|
|
eo.filter(|eo| eo.0 == x).map(|eo| self.height - eo.1 - 1),
|
|
),
|
|
Direction::West => self.west[y].lookup(x, eo.filter(|eo| eo.1 == y).map(|eo| eo.0)),
|
|
})
|
|
.filter(|&r| r != usize::MAX)
|
|
.map(|r| {
|
|
let r = r - 1;
|
|
match direction {
|
|
Direction::North => (x, y - r),
|
|
Direction::East => (x + r, y),
|
|
Direction::South => (x, y + r),
|
|
Direction::West => (x - r, y),
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
fn part1(input: &Input) -> usize {
|
|
Walk::from_input(input)
|
|
.map(|(x, y, _)| (x, y))
|
|
.collect::<FxHashSet<_>>()
|
|
.len()
|
|
}
|
|
|
|
fn part2(input: &Input) -> usize {
|
|
let candidates = Walk::from_input(input)
|
|
.map(|(x, y, _)| (x, y))
|
|
.filter(|&(x, y)| matches!(input[y][x], Cell::Empty))
|
|
.collect::<FxHashSet<_>>();
|
|
|
|
let grid = Grid::from_input(input);
|
|
|
|
candidates
|
|
.into_par_iter()
|
|
.filter(|&(ox, oy)| grid.check_cycle((ox, oy)))
|
|
.count()
|
|
}
|
|
|
|
aoc::main!(2024, 6, ex: 1);
|