use std::{ collections::{HashMap, HashSet}, error::Error, }; use itertools::Itertools; pub fn process_part1(input: &str) -> u32 { simulate_all(input, 100, 2).values().copied().sum() } pub fn process_part2(input: &str) -> u32 { simulate_all(input, 100, 20).values().copied().sum() } fn simulate_all(input: &str, time_to_save: usize, max_cheat_time: usize) -> HashMap { let mut start = (0, 0); let grid = input .lines() .enumerate() .map(|(yidx, row)| { row.chars() .enumerate() .map(|(xidx, chara)| { let tile = GridTile::try_from(chara).unwrap(); if tile == GridTile::Start { start = (xidx, yidx); } tile }) .collect_vec() }) .collect_vec(); let no_cheat = simulate(&grid, start); let mut saved = HashMap::new(); for (tile_idx, tile) in no_cheat[..no_cheat.len() - time_to_save].iter().enumerate() { for (cheat_idx, cheat) in no_cheat[tile_idx..].iter().enumerate() { let manhattan = tile.0.abs_diff(cheat.0) + tile.1.abs_diff(cheat.1); if manhattan <= max_cheat_time { let time_saved = cheat_idx - manhattan; if time_saved >= time_to_save { saved .entry(time_saved as u32) .and_modify(|count| *count += 1) .or_insert(1); } } } } saved } fn simulate(grid: &[Vec], start: (usize, usize)) -> Vec<(usize, usize)> { let mut visited = HashSet::new(); visited.insert(start); let mut next_paths = vec![MazeRunner { coords: start, visited: visited.clone(), ..Default::default() }]; let mut visited = Vec::new(); visited.push(start); let mut arrived: Vec = Vec::new(); while !next_paths.is_empty() { next_paths = next_paths .iter() .map(|maze_runner| { let mut paths = Vec::new(); if let Some(path) = maze_runner.get_next(grid, Direction::Up) { paths.push(path); } if let Some(path) = maze_runner.get_next(grid, Direction::Down) { paths.push(path); } if let Some(path) = maze_runner.get_next(grid, Direction::Left) { paths.push(path); } if let Some(path) = maze_runner.get_next(grid, Direction::Right) { paths.push(path); } paths }) .collect_vec() .concat(); for (idx, maze_runner) in next_paths.clone().iter().enumerate().rev() { if maze_runner.visited.contains(&maze_runner.coords) || visited.contains(&maze_runner.coords) { next_paths.remove(idx); continue; } visited.push(maze_runner.coords); next_paths[idx].visited.insert(maze_runner.coords); if maze_runner.state == State::Arrived { let arrived_reindeer = next_paths.remove(idx); arrived.push(arrived_reindeer); } } } visited } #[derive(Debug, Clone, PartialEq, Eq)] enum Direction { Up, Down, Left, Right, } #[derive(Debug, Default, Clone, PartialEq, Eq)] enum State { #[default] Going, Arrived, } #[derive(Debug, Default, Clone, PartialEq, Eq)] struct MazeRunner { coords: (usize, usize), visited: HashSet<(usize, usize)>, state: State, } impl MazeRunner { fn get_next(&self, grid: &[Vec], direction: Direction) -> Option { let tile = match direction { Direction::Up => grid[self.coords.1 - 1][self.coords.0], Direction::Down => grid[self.coords.1 + 1][self.coords.0], Direction::Right => grid[self.coords.1][self.coords.0 + 1], Direction::Left => grid[self.coords.1][self.coords.0 - 1], }; let coords = match direction { Direction::Up => (self.coords.0, self.coords.1 - 1), Direction::Down => (self.coords.0, self.coords.1 + 1), Direction::Left => (self.coords.0 - 1, self.coords.1), Direction::Right => (self.coords.0 + 1, self.coords.1), }; if tile == GridTile::Wall { None } else if tile == GridTile::End { Some(MazeRunner { state: State::Arrived, coords, ..self.clone() }) } else { Some(MazeRunner { coords, ..self.clone() }) } } } #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] enum GridTile { Wall, #[default] Path, Start, End, } impl TryFrom for GridTile { type Error = Box; fn try_from(value: char) -> std::result::Result> { match value { '#' => Ok(Self::Wall), '.' => Ok(Self::Path), 'S' => Ok(Self::Start), 'E' => Ok(Self::End), _ => Err(Box::from(format!("{value} is not a valid tile"))), } } } #[cfg(test)] mod tests { use std::time::Instant; use utils::time::get_elapsed_string; use super::*; const INPUT: &str = "############### #...#...#.....# #.#.#.#.#.###.# #S#...#.#.#...# #######.#.#.### #######.#.#...# #######.#.###.# ###..E#...#...# ###.#######.### #...###...#...# #.#####.#.###.# #.#...#.#.#...# #.#.#.#.#.#.### #...#...#...### ###############"; #[test] fn part1() { let now = Instant::now(); println!("Test 2:"); let result = simulate_all(INPUT, 2, 2); result .iter() .sorted_by(|a, b| a.0.cmp(b.0)) .for_each(|(saved, count)| { println!("There are {count} cheats that saved {saved} picoseconds"); }); println!("Ran in {}", get_elapsed_string(now.elapsed())); assert_eq!(result.values().copied().sum::(), 44); } #[test] fn part2() { let now = Instant::now(); println!("Test 2:"); let result = simulate_all(INPUT, 50, 20); result .iter() .sorted_by(|a, b| a.0.cmp(b.0)) .for_each(|(saved, count)| { println!("There are {count} cheats that saved {saved} picoseconds"); }); println!("Ran in {}", get_elapsed_string(now.elapsed())); assert_eq!(result.values().copied().sum::(), 285); } }