|
| 1 | +use std::cmp::Reverse; |
| 2 | +use std::collections::BinaryHeap; |
| 3 | +use utils::grid; |
| 4 | +use utils::prelude::*; |
| 5 | + |
| 6 | +/// Finding the shortest paths through a maze. |
| 7 | +#[derive(Clone, Debug)] |
| 8 | +pub struct Day16 { |
| 9 | + grid: Vec<u8>, |
| 10 | + start: usize, |
| 11 | + end: usize, |
| 12 | + offsets: [isize; 4], |
| 13 | + cheapest: Vec<[u32; 4]>, |
| 14 | + part1: u32, |
| 15 | +} |
| 16 | + |
| 17 | +impl Day16 { |
| 18 | + pub fn new(input: &str, _: InputType) -> Result<Self, InputError> { |
| 19 | + let (rows, cols, mut grid) = grid::from_str(input, |b| match b { |
| 20 | + b'.' | b'#' | b'S' | b'E' => Some(b), |
| 21 | + _ => None, |
| 22 | + })?; |
| 23 | + |
| 24 | + if !grid::is_enclosed(rows, cols, &grid, |&b| b == b'#') { |
| 25 | + return Err(InputError::new( |
| 26 | + input, |
| 27 | + 0, |
| 28 | + "expected grid to be enclosed by walls", |
| 29 | + )); |
| 30 | + } |
| 31 | + |
| 32 | + let mut starts = grid.iter().enumerate().filter(|(_, &b)| b == b'S'); |
| 33 | + let Some((start, _)) = starts.next() else { |
| 34 | + return Err(InputError::new(input, 0, "expected one start")); |
| 35 | + }; |
| 36 | + if starts.count() > 0 { |
| 37 | + return Err(InputError::new(input, 0, "expected one start")); |
| 38 | + } |
| 39 | + grid[start] = b'.'; |
| 40 | + |
| 41 | + let mut ends = grid.iter().enumerate().filter(|(_, &b)| b == b'E'); |
| 42 | + let Some((end, _)) = ends.next() else { |
| 43 | + return Err(InputError::new(input, 0, "expected one end")); |
| 44 | + }; |
| 45 | + if ends.count() > 0 { |
| 46 | + return Err(InputError::new(input, 0, "expected one end")); |
| 47 | + } |
| 48 | + grid[end] = b'.'; |
| 49 | + |
| 50 | + let mut instance = Self { |
| 51 | + cheapest: vec![[u32::MAX; 4]; grid.len()], |
| 52 | + part1: 0, |
| 53 | + grid, |
| 54 | + start, |
| 55 | + end, |
| 56 | + offsets: [1, cols as isize, -1, -(cols as isize)], |
| 57 | + }; |
| 58 | + |
| 59 | + // Precompute part 1 as dijkstra output is needed for both parts |
| 60 | + if !instance.dijkstra() { |
| 61 | + return Err(InputError::new(input, 'E', "no path")); |
| 62 | + } |
| 63 | + |
| 64 | + Ok(instance) |
| 65 | + } |
| 66 | + |
| 67 | + fn dijkstra(&mut self) -> bool { |
| 68 | + let mut queue = BinaryHeap::new(); |
| 69 | + queue.push(Reverse((0, self.start, 0))); |
| 70 | + |
| 71 | + while let Some(Reverse((score, index, dir))) = queue.pop() { |
| 72 | + if score > self.cheapest[index][dir] { |
| 73 | + continue; |
| 74 | + } |
| 75 | + if index == self.end { |
| 76 | + self.part1 = score; |
| 77 | + return true; |
| 78 | + } |
| 79 | + |
| 80 | + for (next_dir, next_score) in [ |
| 81 | + (dir, score + 1), |
| 82 | + ((dir + 1) % 4, score + 1001), |
| 83 | + ((dir + 3) % 4, score + 1001), |
| 84 | + ] { |
| 85 | + let next = index.wrapping_add_signed(self.offsets[next_dir]); |
| 86 | + // Advancing to the next branch each time instead of the neighbor reduces the number |
| 87 | + // of items pushed to the priority queue significantly |
| 88 | + if let Some(branch) = self.find_branch(next, next_dir, next_score) { |
| 89 | + // Reverse needed to use BinaryHeap as a min heap and order by the lowest score |
| 90 | + queue.push(Reverse(branch)); |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + false |
| 96 | + } |
| 97 | + |
| 98 | + fn find_branch( |
| 99 | + &mut self, |
| 100 | + mut index: usize, |
| 101 | + mut dir: usize, |
| 102 | + mut score: u32, |
| 103 | + ) -> Option<(u32, usize, usize)> { |
| 104 | + if self.grid[index] != b'.' { |
| 105 | + return None; |
| 106 | + } |
| 107 | + |
| 108 | + while index != self.end { |
| 109 | + let mut count = 0; |
| 110 | + let mut next_index = 0; |
| 111 | + let mut next_dir = 0; |
| 112 | + for d in [dir, (dir + 1) % 4, (dir + 3) % 4] { |
| 113 | + let i = index.wrapping_add_signed(self.offsets[d]); |
| 114 | + if self.grid[i] == b'.' { |
| 115 | + count += 1; |
| 116 | + next_index = i; |
| 117 | + next_dir = d; |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + if count == 0 { |
| 122 | + return None; |
| 123 | + } else if count > 1 { |
| 124 | + break; |
| 125 | + } |
| 126 | + |
| 127 | + score += if dir == next_dir { 1 } else { 1001 }; |
| 128 | + index = next_index; |
| 129 | + dir = next_dir; |
| 130 | + } |
| 131 | + |
| 132 | + if score < self.cheapest[index][dir] { |
| 133 | + self.cheapest[index][dir] = score; |
| 134 | + Some((score, index, dir)) |
| 135 | + } else { |
| 136 | + None |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + #[must_use] |
| 141 | + pub fn part1(&self) -> u32 { |
| 142 | + self.part1 |
| 143 | + } |
| 144 | + |
| 145 | + #[must_use] |
| 146 | + pub fn part2(&self) -> u32 { |
| 147 | + let mut on_best = vec![false; self.grid.len()]; |
| 148 | + on_best[self.start] = true; |
| 149 | + on_best[self.end] = true; |
| 150 | + for d in 0..4 { |
| 151 | + if self.cheapest[self.end][d] == self.part1 { |
| 152 | + let prev = self.end.wrapping_add_signed(-self.offsets[d]); |
| 153 | + self.reverse(prev, d, self.part1 - 1, &mut on_best); |
| 154 | + } |
| 155 | + } |
| 156 | + on_best.iter().filter(|&&b| b).count() as u32 |
| 157 | + } |
| 158 | + |
| 159 | + fn reverse(&self, index: usize, dir: usize, score: u32, on_best: &mut [bool]) { |
| 160 | + if on_best[index] { |
| 161 | + return; |
| 162 | + } |
| 163 | + on_best[index] = true; |
| 164 | + |
| 165 | + let mut count = 0; |
| 166 | + let mut next_index = 0; |
| 167 | + let mut next_dir = 0; |
| 168 | + for d in [dir, (dir + 1) % 4, (dir + 3) % 4] { |
| 169 | + let i = index.wrapping_add_signed(-self.offsets[d]); |
| 170 | + if self.grid[i] == b'.' { |
| 171 | + count += 1; |
| 172 | + next_index = i; |
| 173 | + next_dir = d; |
| 174 | + } |
| 175 | + } |
| 176 | + assert!(count > 0); |
| 177 | + |
| 178 | + if count == 1 { |
| 179 | + let next_score = score - if dir == next_dir { 1 } else { 1001 }; |
| 180 | + self.reverse(next_index, next_dir, next_score, on_best); |
| 181 | + } else { |
| 182 | + // At a branch, only continue down directions where the cheapest seen score matches |
| 183 | + for (next_dir, next_score) in [ |
| 184 | + (dir, score), |
| 185 | + ((dir + 1) % 4, score - 1000), |
| 186 | + ((dir + 3) % 4, score - 1000), |
| 187 | + ] { |
| 188 | + if self.cheapest[index][next_dir] == next_score { |
| 189 | + self.reverse( |
| 190 | + index.wrapping_add_signed(-self.offsets[next_dir]), |
| 191 | + next_dir, |
| 192 | + next_score - 1, |
| 193 | + on_best, |
| 194 | + ); |
| 195 | + } |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | +} |
| 200 | + |
| 201 | +examples!(Day16 -> (u32, u32) [ |
| 202 | + {file: "day16_example0.txt", part1: 7036, part2: 45}, |
| 203 | + {file: "day16_example1.txt", part1: 11048, part2: 64}, |
| 204 | +]); |
0 commit comments