diff --git a/src/bin/day16.rs b/src/bin/day16.rs new file mode 100644 index 0000000..b227627 --- /dev/null +++ b/src/bin/day16.rs @@ -0,0 +1,266 @@ +use std::{ + collections::{BinaryHeap, HashMap, HashSet}, + hash::Hash, + time::Instant, +}; + +use aoc_2024::{load, print_res, Direction}; +use arrayvec::ArrayVec; +use bstr::{BString, ByteSlice}; +use color_eyre::eyre::{bail, ContextCompat}; + +#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)] +pub struct Point { + line: usize, + col: usize, +} + +type Parsed = (Point, Point, Vec>); + +#[inline(never)] +pub fn parsing(input: &BString) -> color_eyre::Result { + let mut start = None; + let mut end = None; + let mut grid = Vec::new(); + + for (l, line) in input.lines().enumerate() { + let mut row = Vec::new(); + for (c, &t) in line.iter().enumerate() { + match t { + b'#' => row.push(false), + b'.' => row.push(true), + b'S' => { + start = Some(Point { line: l, col: c }); + row.push(true); + } + b'E' => { + end = Some(Point { line: l, col: c }); + row.push(true); + } + _ => bail!("Unknown char: `{}`", t as char), + } + } + grid.push(row); + } + + Ok(( + start.with_context(|| "Could not find start")?, + end.with_context(|| "Could not find end")?, + grid, + )) +} + +#[derive(Debug)] +struct PathInfo { + cost: u64, + from: ArrayVec<(Direction, Point), 4>, +} + +struct BestPaths { + paths: HashMap<(Direction, Point), PathInfo>, +} + +fn best_paths((start, end, grid): Parsed) -> BestPaths { + #[derive(Debug)] + struct Position { + cost: u64, + direction: Direction, + current: Point, + } + + impl PartialEq for Position { + fn eq(&self, other: &Self) -> bool { + self.cost == other.cost + } + } + + impl Eq for Position {} + + impl PartialOrd for Position { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for Position { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other.cost.cmp(&self.cost) + } + } + + fn try_visit( + from: (Direction, Point), + dest: Position, + seen: &mut HashMap<(Direction, Point), PathInfo>, + paths: &mut BinaryHeap, + ) { + match seen.get_mut(&(dest.direction, dest.current)) { + Some(info) if info.cost < dest.cost => (), + Some(info) if info.cost == dest.cost => { + if !info.from.contains(&from) { + info.from.push(from); + } + } + _ => { + let mut f = ArrayVec::new(); + f.push(from); + + seen.insert( + (dest.direction, dest.current), + PathInfo { + cost: dest.cost, + from: f, + }, + ); + paths.push(dest); + } + } + } + + let mut seen = HashMap::new(); + seen.insert( + (Direction::Left, start), + PathInfo { + cost: 0, + from: ArrayVec::new(), + }, + ); + + let mut paths = BinaryHeap::new(); + paths.push(Position { + cost: 0, + direction: Direction::Left, + current: start, + }); + + let mut end_cost = None; + + loop { + let best = paths.pop().unwrap(); + + if let Some(cost) = end_cost { + if best.cost > cost { + return BestPaths { paths: seen }; + } + } + + if best.current == end { + match end_cost { + Some(cost) if cost < best.cost => continue, + _ => end_cost = Some(best.cost), + } + } + + match seen.get(&(best.direction, best.current)) { + Some(info) if info.cost < best.cost => continue, + _ => (), + } + + let (line, col) = best.direction.walk(best.current.line, best.current.col); + if grid[line][col] { + try_visit( + (best.direction, best.current), + Position { + cost: best.cost + 1, + direction: best.direction, + current: Point { line, col }, + }, + &mut seen, + &mut paths, + ); + } + + try_visit( + (best.direction, best.current), + Position { + cost: best.cost + 1000, + direction: best.direction.turn_right(), + current: best.current, + }, + &mut seen, + &mut paths, + ); + try_visit( + (best.direction, best.current), + Position { + cost: best.cost + 1000, + direction: best.direction.turn_left(), + current: best.current, + }, + &mut seen, + &mut paths, + ); + } +} + +impl BestPaths { + fn best_cost(&self, at: Point) -> Option<&PathInfo> { + Direction::all() + .iter() + .filter_map(|&d| self.paths.get(&(d, at))) + .min_by_key(|i| i.cost) + } +} + +#[inline(never)] +pub fn part1(input: Parsed) { + let end = input.1; + let best_paths = best_paths(input); + + print_res!("Fastest path: {}", best_paths.best_cost(end).unwrap().cost); +} + +impl BestPaths { + fn retrace(&self, from: Point) -> HashSet { + let from_info = self.best_cost(from).unwrap(); + let mut seen = HashSet::new(); + seen.insert(from); + + let mut paths = from_info.from.to_vec(); + while !paths.is_empty() { + let mut new_paths = Vec::new(); + + for path in paths { + seen.insert(path.1); + let info = self.paths.get(&path).unwrap(); + new_paths.extend(info.from.clone()); + } + + paths = new_paths; + } + + seen + } +} + +#[inline(never)] +pub fn part2(input: Parsed) { + let end = input.1; + let best_paths = best_paths(input); + + print_res!( + "Tile count in best paths: {}", + best_paths.retrace(end).len() + ); +} + +pub fn main() -> color_eyre::Result<()> { + let context = load()?; + + let start = Instant::now(); + let parsed = parsing(&context.input)?; + let elapsed = humantime::format_duration(start.elapsed()); + + let start = Instant::now(); + if context.part == 1 { + part1(parsed); + } else { + part2(parsed); + } + let elapsed_part = humantime::format_duration(start.elapsed()); + + println!(" Parsing: {elapsed}"); + println!(" Solving: {elapsed_part}"); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 36c6c39..7af1ebe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,15 @@ impl Direction { } } + pub fn turn_left(self) -> Self { + match self { + Self::Up => Self::Left, + Self::Left => Self::Down, + Self::Down => Self::Right, + Self::Right => Self::Up, + } + } + pub fn walk(&self, l: usize, c: usize) -> (usize, usize) { match self { Direction::Up => (l - 1, c),