use std::{collections::HashMap, error::Error}; use itertools::Itertools; pub fn process_part1(input: &str) -> usize { let mut start = (0, 0); let mut end = (0, 0); let grid = input .lines() .enumerate() .map(|(yidx, row)| { row.chars() .enumerate() .map(|(xidx, chara)| { let tile = GridTile::try_from(chara).unwrap(); match tile { GridTile::Wall | GridTile::Path => tile, GridTile::Start => { start = (xidx, yidx); tile } GridTile::End => { end = (xidx, yidx); tile } } }) .collect_vec() }) .collect_vec(); let mut next_paths = vec![Reindeer { coords: start, ..Default::default() }]; let mut arrived: Vec = Vec::new(); let mut visited = HashMap::new(); let mut iter = 0; while !next_paths.is_empty() { next_paths = next_paths .iter() .map(|reindeer| reindeer.get_all_next_paths(&grid)) .collect_vec() .concat(); for (idx, reindeer) in next_paths.clone().iter().enumerate().rev() { if let Some(score) = visited.get_mut(&reindeer.coords) { if *score <= reindeer.score { next_paths.remove(idx); continue; } else { *score = reindeer.score; } } else { visited.insert(reindeer.coords, reindeer.score); } if reindeer.state == ReindeerState::Arrived { let arrived_reindeer = next_paths.remove(idx); arrived.push(arrived_reindeer); } } iter += 1; } println!("Iterations to goal {iter}"); arrived.iter().map(|reindeer| reindeer.score).min().unwrap() } pub fn process_part2(input: &str) -> usize { let mut start = (0, 0); let mut end = (0, 0); let grid = input .lines() .enumerate() .map(|(yidx, row)| { row.chars() .enumerate() .map(|(xidx, chara)| { let tile = GridTile::try_from(chara).unwrap(); match tile { GridTile::Wall | GridTile::Path => tile, GridTile::Start => { start = (xidx, yidx); tile } GridTile::End => { end = (xidx, yidx); tile } } }) .collect_vec() }) .collect_vec(); let mut smallest_score = usize::MAX; let mut visited = HashMap::new(); visited.insert((start, Orientation::East), 0); let mut next_paths = vec![Reindeer { coords: start, visited: visited.clone(), ..Default::default() }]; let mut arrived: Vec = Vec::new(); while !next_paths.is_empty() { next_paths = next_paths .iter() .map(|reindeer| reindeer.get_all_next_paths(&grid)) .collect_vec() .concat(); for (idx, reindeer) in next_paths.clone().iter().enumerate().rev() { if reindeer.score > smallest_score || reindeer .visited .contains_key(&(reindeer.coords, reindeer.orientation)) { next_paths.remove(idx); continue; } next_paths[idx] .visited .insert((reindeer.coords, reindeer.orientation), reindeer.score); if let Some(score) = visited.get_mut(&(reindeer.coords, reindeer.orientation)) { if *score < reindeer.score { next_paths.remove(idx); continue; } else { *score = reindeer.score; } } else { visited.insert((reindeer.coords, reindeer.orientation), reindeer.score); } if reindeer.state == ReindeerState::Arrived && reindeer.score <= smallest_score { smallest_score = reindeer.score; let arrived_reindeer = next_paths.remove(idx); arrived.push(arrived_reindeer); } } arrived.retain(|reindeer| reindeer.score == smallest_score); } let visited = arrived .iter() .map(|reindeer| { reindeer .visited .keys() .map(|(coords, _)| coords) .collect_vec() }) .collect_vec() .concat(); log_maze(&grid, &arrived); visited.iter().unique().count() } #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] enum Orientation { #[default] East, West, North, South, } impl Orientation { fn turn_clockwise(&self) -> Self { match self { Orientation::East => Self::South, Orientation::West => Self::North, Orientation::North => Self::East, Orientation::South => Self::West, } } fn turn_counterclockwise(&self) -> Self { match self { Orientation::East => Self::North, Orientation::West => Self::South, Orientation::North => Self::West, Orientation::South => Self::East, } } } #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] enum ReindeerState { #[default] Going, Arrived, Stuck, } #[derive(Debug, Default, Clone, PartialEq, Eq)] struct Reindeer { orientation: Orientation, coords: (usize, usize), score: usize, visited: HashMap<((usize, usize), Orientation), usize>, state: ReindeerState, } impl Reindeer { fn get_next(&self, grid: &[Vec]) -> Reindeer { let tile = match self.orientation { Orientation::East => grid[self.coords.1][self.coords.0 + 1], Orientation::West => grid[self.coords.1][self.coords.0 - 1], Orientation::North => grid[self.coords.1 - 1][self.coords.0], Orientation::South => grid[self.coords.1 + 1][self.coords.0], }; let coords = match self.orientation { Orientation::East => (self.coords.0 + 1, self.coords.1), Orientation::West => (self.coords.0 - 1, self.coords.1), Orientation::South => (self.coords.0, self.coords.1 + 1), Orientation::North => (self.coords.0, self.coords.1 - 1), }; if tile == GridTile::Wall { Reindeer { state: ReindeerState::Stuck, ..self.clone() } } else if tile == GridTile::End { Reindeer { score: self.score + 1, state: ReindeerState::Arrived, coords, ..self.clone() } } else { Reindeer { score: self.score + 1, coords, ..self.clone() } } } fn get_right(&self, grid: &[Vec]) -> Reindeer { let tile = match self.orientation { Orientation::East => grid[self.coords.1 + 1][self.coords.0], Orientation::West => grid[self.coords.1 - 1][self.coords.0], Orientation::North => grid[self.coords.1][self.coords.0 + 1], Orientation::South => grid[self.coords.1][self.coords.0 - 1], }; let coords = match self.orientation { Orientation::East => (self.coords.0, self.coords.1 + 1), Orientation::West => (self.coords.0, self.coords.1 - 1), Orientation::South => (self.coords.0 - 1, self.coords.1), Orientation::North => (self.coords.0 + 1, self.coords.1), }; if tile == GridTile::Wall { Reindeer { state: ReindeerState::Stuck, ..self.clone() } } else if tile == GridTile::End { Reindeer { score: self.score + 1001, state: ReindeerState::Arrived, coords, orientation: self.orientation.turn_clockwise(), ..self.clone() } } else { Reindeer { score: self.score + 1001, orientation: self.orientation.turn_clockwise(), coords, ..self.clone() } } } fn get_left(&self, grid: &[Vec]) -> Reindeer { let tile = match self.orientation { Orientation::East => grid[self.coords.1 - 1][self.coords.0], Orientation::West => grid[self.coords.1 + 1][self.coords.0], Orientation::North => grid[self.coords.1][self.coords.0 - 1], Orientation::South => grid[self.coords.1][self.coords.0 + 1], }; let coords = match self.orientation { Orientation::East => (self.coords.0, self.coords.1 - 1), Orientation::West => (self.coords.0, self.coords.1 + 1), Orientation::South => (self.coords.0 + 1, self.coords.1), Orientation::North => (self.coords.0 - 1, self.coords.1), }; if tile == GridTile::Wall { Reindeer { state: ReindeerState::Stuck, ..self.clone() } } else if tile == GridTile::End { Reindeer { score: self.score + 1001, state: ReindeerState::Arrived, coords, orientation: self.orientation.turn_counterclockwise(), ..self.clone() } } else { Reindeer { score: self.score + 1001, orientation: self.orientation.turn_counterclockwise(), coords, ..self.clone() } } } fn get_all_next_paths(&self, grid: &[Vec]) -> Vec { let next = self.get_next(grid); let right = self.get_right(grid); let left = self.get_left(grid); let mut paths = Vec::new(); if next.state != ReindeerState::Stuck { paths.push(next); } if right.state != ReindeerState::Stuck { paths.push(right); } if left.state != ReindeerState::Stuck { paths.push(left); } paths } } #[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"))), } } } fn log_maze(grid: &[Vec], visited: &Vec) { for (yidx, row) in grid.iter().enumerate() { for (xidx, tile) in row.iter().enumerate() { let contains = { let mut contains = false; for reindeer in visited { if reindeer .visited .contains_key(&((xidx, yidx), Orientation::East)) || reindeer .visited .contains_key(&((xidx, yidx), Orientation::West)) || reindeer .visited .contains_key(&((xidx, yidx), Orientation::North)) || reindeer .visited .contains_key(&((xidx, yidx), Orientation::South)) { contains = true; break; } } contains }; if contains && !(*tile == GridTile::Start || *tile == GridTile::End) { print!("O"); } else if *tile == GridTile::Wall { print!("#"); } else if *tile == GridTile::Path { print!("."); } else if *tile == GridTile::Start { print!("S"); } else if *tile == GridTile::End { print!("E"); } } println!(); } } #[cfg(test)] mod tests { use super::*; const INPUT_1: &str = "############### #.......#....E# #.#.###.#.###.# #.....#.#...#.# #.###.#####.#.# #.#.#.......#.# #.#.#####.###.# #...........#.# ###.#.#####.#.# #...#.....#.#.# #.#.#.###.#.#.# #.....#...#.#.# #.###.#.#.#.#.# #S..#.....#...# ###############"; const INPUT_2: &str = "################# #...#...#...#..E# #.#.#.#.#.#.#.#.# #.#.#.#...#...#.# #.#.#.#.###.#.#.# #...#.#.#.....#.# #.#.#.#.#.#####.# #.#...#.#.#.....# #.#.#####.#.###.# #.#.#.......#...# #.#.###.#####.### #.#.#...#.....#.# #.#.#.#####.###.# #.#.#.........#.# #.#.#.#########.# #S#.............# #################"; #[test] fn part1_1() { let result = process_part1(INPUT_1); assert_eq!(result, 7036); } #[test] fn part1_2() { let result = process_part1(INPUT_2); assert_eq!(result, 11048); } #[test] fn part2_1() { let result = process_part2(INPUT_1); assert_eq!(result, 45); } #[test] fn part2_2() { let result = process_part2(INPUT_2); assert_eq!(result, 64); } }