|
| 1 | +use std::{ |
| 2 | + cmp::Ordering, |
| 3 | + collections::{BinaryHeap, HashMap, HashSet}, |
| 4 | +}; |
| 5 | + |
| 6 | +use crate::solution::{AocError, Solution}; |
| 7 | + |
| 8 | +type Coords = (usize, usize); |
| 9 | +type Grid = Vec<Vec<bool>>; |
| 10 | + |
| 11 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 12 | +enum Direction { |
| 13 | + North, |
| 14 | + East, |
| 15 | + South, |
| 16 | + West, |
| 17 | +} |
| 18 | + |
| 19 | +impl Direction { |
| 20 | + fn as_delta(&self) -> (isize, isize) { |
| 21 | + match self { |
| 22 | + Direction::North => (0, -1), |
| 23 | + Direction::East => (1, 0), |
| 24 | + Direction::South => (0, 1), |
| 25 | + Direction::West => (-1, 0), |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | + fn rotate_left(&self) -> Direction { |
| 30 | + match self { |
| 31 | + Direction::North => Direction::West, |
| 32 | + Direction::East => Direction::North, |
| 33 | + Direction::South => Direction::East, |
| 34 | + Direction::West => Direction::South, |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + fn rotate_right(&self) -> Direction { |
| 39 | + match self { |
| 40 | + Direction::North => Direction::East, |
| 41 | + Direction::East => Direction::South, |
| 42 | + Direction::South => Direction::West, |
| 43 | + Direction::West => Direction::North, |
| 44 | + } |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +#[derive(Clone, Eq, PartialEq)] |
| 49 | +struct Search { |
| 50 | + score: u32, |
| 51 | + position: Coords, |
| 52 | + direction: Direction, |
| 53 | + visited: HashSet<Coords>, |
| 54 | +} |
| 55 | + |
| 56 | +impl Ord for Search { |
| 57 | + fn cmp(&self, other: &Self) -> Ordering { |
| 58 | + other.score.cmp(&self.score) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +impl PartialOrd for Search { |
| 63 | + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| 64 | + Some(self.cmp(other)) |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +fn parse(input: &str) -> Result<(Grid, Coords, Coords), AocError> { |
| 69 | + let mut grid = vec![]; |
| 70 | + let mut start = None; |
| 71 | + let mut end = None; |
| 72 | + |
| 73 | + for (y, line) in input.trim().lines().enumerate() { |
| 74 | + let mut row = vec![]; |
| 75 | + for (x, tile) in line.chars().enumerate() { |
| 76 | + match tile { |
| 77 | + '#' => row.push(false), |
| 78 | + '.' => row.push(true), |
| 79 | + 'S' => { |
| 80 | + row.push(true); |
| 81 | + start = Some((x, y)) |
| 82 | + } |
| 83 | + 'E' => { |
| 84 | + row.push(true); |
| 85 | + end = Some((x, y)) |
| 86 | + } |
| 87 | + other => return Err(AocError::parse(other, "Unexpected tile")), |
| 88 | + } |
| 89 | + } |
| 90 | + grid.push(row); |
| 91 | + } |
| 92 | + |
| 93 | + match (start, end) { |
| 94 | + (Some(start), Some(end)) => Ok((grid, start, end)), |
| 95 | + _ => Err(AocError::parse(input, "Missing start or end")), |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +fn dijkstra(grid: &Grid, start: Coords, end: Coords, find_all: bool) -> Option<u32> { |
| 100 | + let mut scores: HashMap<(Coords, Direction), u32> = HashMap::new(); |
| 101 | + let mut heap: BinaryHeap<Search> = BinaryHeap::new(); |
| 102 | + |
| 103 | + let height = grid.len() as isize; |
| 104 | + let width = grid[0].len() as isize; |
| 105 | + |
| 106 | + heap.push(Search { |
| 107 | + score: 0, |
| 108 | + direction: Direction::East, |
| 109 | + position: start, |
| 110 | + visited: HashSet::new(), |
| 111 | + }); |
| 112 | + |
| 113 | + let mut best_score: Option<u32> = None; |
| 114 | + let mut best_path_tiles: HashSet<Coords> = HashSet::new(); |
| 115 | + |
| 116 | + while let Some(Search { |
| 117 | + score, |
| 118 | + direction, |
| 119 | + position, |
| 120 | + mut visited, |
| 121 | + }) = heap.pop() |
| 122 | + { |
| 123 | + if !visited.insert(position) { |
| 124 | + continue; |
| 125 | + } |
| 126 | + |
| 127 | + if score > *scores.get(&(position, direction)).unwrap_or(&u32::MAX) { |
| 128 | + continue; |
| 129 | + } |
| 130 | + |
| 131 | + if position == end { |
| 132 | + if find_all { |
| 133 | + match best_score { |
| 134 | + None => { |
| 135 | + best_score = Some(score); |
| 136 | + best_path_tiles.extend(visited.iter()); |
| 137 | + } |
| 138 | + Some(best) if best == score => { |
| 139 | + best_path_tiles.extend(visited.iter()); |
| 140 | + } |
| 141 | + _ => {} |
| 142 | + } |
| 143 | + } else { |
| 144 | + return Some(score); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + for (next_direction, turn_cost) in [ |
| 149 | + (direction, 0), |
| 150 | + (direction.rotate_left(), 1000), |
| 151 | + (direction.rotate_right(), 1000), |
| 152 | + ] { |
| 153 | + let next_score = score + turn_cost + 1; |
| 154 | + |
| 155 | + let (dx, dy) = next_direction.as_delta(); |
| 156 | + let (x, y) = (position.0 as isize + dx, position.1 as isize + dy); |
| 157 | + |
| 158 | + if x < 0 || y < 0 || x >= width || y >= height { |
| 159 | + continue; |
| 160 | + } |
| 161 | + |
| 162 | + if !grid[y as usize][x as usize] { |
| 163 | + continue; |
| 164 | + } |
| 165 | + |
| 166 | + let next_pos = (x as usize, y as usize); |
| 167 | + let best_known = scores.entry((next_pos, next_direction)).or_insert(u32::MAX); |
| 168 | + |
| 169 | + if next_score <= *best_known { |
| 170 | + *best_known = next_score; |
| 171 | + |
| 172 | + heap.push(Search { |
| 173 | + score: next_score, |
| 174 | + direction: next_direction, |
| 175 | + position: next_pos, |
| 176 | + visited: visited.clone(), |
| 177 | + }); |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + if find_all { |
| 183 | + return Some(best_path_tiles.len() as u32); |
| 184 | + } |
| 185 | + |
| 186 | + None |
| 187 | +} |
| 188 | + |
| 189 | +pub struct Day16; |
| 190 | +impl Solution for Day16 { |
| 191 | + type A = u32; |
| 192 | + type B = u32; |
| 193 | + |
| 194 | + fn default_input(&self) -> &'static str { |
| 195 | + include_str!("../../../inputs/2024/day16.txt") |
| 196 | + } |
| 197 | + |
| 198 | + fn part_1(&self, input: &str) -> Result<u32, AocError> { |
| 199 | + let (grid, start, end) = parse(input)?; |
| 200 | + |
| 201 | + let smallest_score = |
| 202 | + dijkstra(&grid, start, end, false).ok_or_else(|| AocError::logic("No path found"))?; |
| 203 | + |
| 204 | + Ok(smallest_score) |
| 205 | + } |
| 206 | + |
| 207 | + fn part_2(&self, input: &str) -> Result<u32, AocError> { |
| 208 | + let (grid, start, end) = parse(input)?; |
| 209 | + |
| 210 | + let smallest_score = |
| 211 | + dijkstra(&grid, start, end, true).ok_or_else(|| AocError::logic("No path found"))?; |
| 212 | + |
| 213 | + Ok(smallest_score) |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +#[cfg(test)] |
| 218 | +mod tests { |
| 219 | + use super::*; |
| 220 | + |
| 221 | + #[test] |
| 222 | + fn it_solves_part1_example_1() { |
| 223 | + assert_eq!( |
| 224 | + Day16.part_1( |
| 225 | + "###############\n\ |
| 226 | + #.......#....E#\n\ |
| 227 | + #.#.###.#.###.#\n\ |
| 228 | + #.....#.#...#.#\n\ |
| 229 | + #.###.#####.#.#\n\ |
| 230 | + #.#.#.......#.#\n\ |
| 231 | + #.#.#####.###.#\n\ |
| 232 | + #...........#.#\n\ |
| 233 | + ###.#.#####.#.#\n\ |
| 234 | + #...#.....#.#.#\n\ |
| 235 | + #.#.#.###.#.#.#\n\ |
| 236 | + #.....#...#.#.#\n\ |
| 237 | + #.###.#.#.#.#.#\n\ |
| 238 | + #S..#.....#...#\n\ |
| 239 | + ###############" |
| 240 | + ), |
| 241 | + Ok(7036) |
| 242 | + ); |
| 243 | + } |
| 244 | + |
| 245 | + #[test] |
| 246 | + fn it_solves_part1_example_2() { |
| 247 | + assert_eq!( |
| 248 | + Day16.part_1( |
| 249 | + "#################\n\ |
| 250 | + #...#...#...#..E#\n\ |
| 251 | + #.#.#.#.#.#.#.#.#\n\ |
| 252 | + #.#.#.#...#...#.#\n\ |
| 253 | + #.#.#.#.###.#.#.#\n\ |
| 254 | + #...#.#.#.....#.#\n\ |
| 255 | + #.#.#.#.#.#####.#\n\ |
| 256 | + #.#...#.#.#.....#\n\ |
| 257 | + #.#.#####.#.###.#\n\ |
| 258 | + #.#.#.......#...#\n\ |
| 259 | + #.#.###.#####.###\n\ |
| 260 | + #.#.#...#.....#.#\n\ |
| 261 | + #.#.#.#####.###.#\n\ |
| 262 | + #.#.#.........#.#\n\ |
| 263 | + #.#.#.#########.#\n\ |
| 264 | + #S#.............#\n\ |
| 265 | + #################" |
| 266 | + ), |
| 267 | + Ok(11048) |
| 268 | + ); |
| 269 | + } |
| 270 | + |
| 271 | + #[test] |
| 272 | + fn it_solves_part2_example_1() { |
| 273 | + assert_eq!( |
| 274 | + Day16.part_2( |
| 275 | + "###############\n\ |
| 276 | + #.......#....E#\n\ |
| 277 | + #.#.###.#.###.#\n\ |
| 278 | + #.....#.#...#.#\n\ |
| 279 | + #.###.#####.#.#\n\ |
| 280 | + #.#.#.......#.#\n\ |
| 281 | + #.#.#####.###.#\n\ |
| 282 | + #...........#.#\n\ |
| 283 | + ###.#.#####.#.#\n\ |
| 284 | + #...#.....#.#.#\n\ |
| 285 | + #.#.#.###.#.#.#\n\ |
| 286 | + #.....#...#.#.#\n\ |
| 287 | + #.###.#.#.#.#.#\n\ |
| 288 | + #S..#.....#...#\n\ |
| 289 | + ###############" |
| 290 | + ), |
| 291 | + Ok(45) |
| 292 | + ); |
| 293 | + } |
| 294 | + |
| 295 | + #[test] |
| 296 | + fn it_solves_part2_example_2() { |
| 297 | + assert_eq!( |
| 298 | + Day16.part_2( |
| 299 | + "#################\n\ |
| 300 | + #...#...#...#..E#\n\ |
| 301 | + #.#.#.#.#.#.#.#.#\n\ |
| 302 | + #.#.#.#...#...#.#\n\ |
| 303 | + #.#.#.#.###.#.#.#\n\ |
| 304 | + #...#.#.#.....#.#\n\ |
| 305 | + #.#.#.#.#.#####.#\n\ |
| 306 | + #.#...#.#.#.....#\n\ |
| 307 | + #.#.#####.#.###.#\n\ |
| 308 | + #.#.#.......#...#\n\ |
| 309 | + #.#.###.#####.###\n\ |
| 310 | + #.#.#...#.....#.#\n\ |
| 311 | + #.#.#.#####.###.#\n\ |
| 312 | + #.#.#.........#.#\n\ |
| 313 | + #.#.#.#########.#\n\ |
| 314 | + #S#.............#\n\ |
| 315 | + #################" |
| 316 | + ), |
| 317 | + Ok(64) |
| 318 | + ); |
| 319 | + } |
| 320 | +} |
0 commit comments