diff --git a/src/bin/day20.rs b/src/bin/day20.rs new file mode 100644 index 0000000..7448fa6 --- /dev/null +++ b/src/bin/day20.rs @@ -0,0 +1,178 @@ +use std::{ + collections::{HashMap, HashSet}, + time::Instant, +}; + +use aoc_2024::{load, neighbours, print_res}; +use bstr::{BString, ByteSlice}; +use color_eyre::eyre::{bail, ensure, OptionExt}; + +#[derive(Debug)] +pub struct Racetrack { + grid: Vec>, + end: (usize, usize), +} + +type Parsed = Racetrack; + +#[inline(never)] +pub fn parsing(input: &BString) -> color_eyre::Result { + let mut grid = Vec::new(); + let mut end = None; + + for (l, line) in input.lines().enumerate() { + let mut grid_line = Vec::new(); + + for (c, &v) in line.iter().enumerate() { + grid_line.push(match v { + b'.' | b'S' => true, + b'#' => false, + b'E' => { + ensure!(end.is_none(), "Duplicate end"); + end = Some((l, c)); + true + } + _ => bail!("Unknown character: {}", v as char), + }); + } + + grid.push(grid_line); + } + + Ok(Racetrack { + grid, + end: end.ok_or_eyre("Missing end")?, + }) +} + +const CUTOFF: usize = 100; + +impl Racetrack { + fn distances_from_end(&self) -> HashMap<(usize, usize), usize> { + let mut distances = HashMap::new(); + + let mut current = HashSet::new(); + current.insert(self.end); + let mut dist = 0; + + while !current.is_empty() { + let mut next = HashSet::new(); + + for (l, c) in current { + distances.insert((l, c), dist); + + for (nl, nc) in neighbours(l, c, &self.grid) { + if self.grid[nl][nc] && !distances.contains_key(&(nl, nc)) { + next.insert((nl, nc)); + } + } + } + + current = next; + dist += 1; + } + + distances + } +} + +#[inline(never)] +pub fn part1(input: Parsed) { + let distances = input.distances_from_end(); + + let mut saved_count = HashMap::<_, u64>::new(); + + for (&(cheat_l, cheat_c), &start_dist) in &distances { + for (nl, nc) in neighbours(cheat_l, cheat_c, &input.grid) { + for (nnl, nnc) in neighbours(nl, nc, &input.grid) { + if !input.grid[nnl][nnc] { + continue; + } + + let end_dist = distances[&(nnl, nnc)]; + + // Did not gain anything with this cheat + if start_dist + 2 >= end_dist { + continue; + } + + let saved = end_dist - start_dist - 2; + *saved_count.entry(saved).or_default() += 1; + } + } + } + + let major: u64 = saved_count + .iter() + .filter_map(|(&s, &occ)| if s >= CUTOFF { Some(occ) } else { None }) + .sum(); + + print_res!("Number of ways to cheat saving 100ps: {major}"); +} + +#[inline(never)] +pub fn part2(input: Parsed) { + let distances = input.distances_from_end(); + + let mut saved_count = HashMap::<_, u64>::new(); + + for (&(cheat_l, cheat_c), &start_dist) in &distances { + let min_l = cheat_l.saturating_sub(20); + let max_l = std::cmp::min(cheat_l + 20, input.grid.len() - 1); + + let min_c = cheat_c.saturating_sub(20); + let max_c = std::cmp::min(cheat_c + 20, input.grid[0].len() - 1); + + for el in min_l..=max_l { + for ec in min_c..=max_c { + let distance = cheat_l.abs_diff(el) + cheat_c.abs_diff(ec); + + if distance > 20 { + continue; + } + + if !input.grid[el][ec] { + continue; + } + + let end_dist = distances[&(el, ec)]; + + // Did not gain anything with this cheat + if start_dist + 20 >= end_dist { + continue; + } + + let saved = end_dist - start_dist - distance; + *saved_count.entry(saved).or_default() += 1; + } + } + } + + let major: u64 = saved_count + .iter() + .filter_map(|(&s, &occ)| if s >= CUTOFF { Some(occ) } else { None }) + .sum(); + + print_res!("Number of ways to cheat saving 100ps: {major}"); +} + +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(()) +}