1use std::cmp::Reverse;
2use std::collections::BinaryHeap;
3use utils::array::ArrayVec;
4use utils::grid;
5use utils::prelude::*;
6
7#[derive(Clone, Debug)]
12pub struct Day20 {
13 graph: Vec<Vec<Edge>>,
14 portals: Vec<Portal>,
15}
16
17#[derive(Clone, Copy, Debug, Default)]
18struct Edge {
19 target: u16,
20 distance: u16,
21}
22
23#[derive(Clone, Copy, Debug)]
24struct Portal {
25 exit: u16,
26 outer: bool,
27}
28
29const LABEL_COUNT: usize = 26 * 26;
30const AA: usize = 0;
31const ZZ: usize = LABEL_COUNT - 1;
32const START: usize = 0;
33const END: usize = 1;
34
35impl Day20 {
36 pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
37 let (rows, cols, grid) = grid::parse(
38 input,
39 1,
40 b' ',
41 |b| b,
42 |b| matches!(b, b' ' | b'#' | b'.' | b'A'..=b'Z'),
43 |_, _| Err("expected space, '#', '.' or uppercase letter"),
44 )?;
45 if grid.len() >= usize::from(u16::MAX) {
46 return Err(InputError::new(input, 0, "grid too large"));
47 }
48
49 let mut endpoints = [[u16::MAX; 2]; LABEL_COUNT];
50 let mut endpoint_count = [0u8; LABEL_COUNT];
51 let mut portal_nodes = vec![u16::MAX; grid.len()];
52
53 for (index, &tile) in grid.iter().enumerate() {
55 if !tile.is_ascii_uppercase() {
56 continue;
57 }
58
59 let letter_neighbours = [index - 1, index + 1, index - cols, index + cols]
60 .into_iter()
61 .filter(|&next| grid[next].is_ascii_uppercase())
62 .count();
63 if letter_neighbours != 1 {
64 return Err(InputError::new(
65 input,
66 0,
67 "expected portal labels to contain two letters",
68 ));
69 }
70
71 let (second, before, after) = if grid[index + 1].is_ascii_uppercase() {
72 (index + 1, index - 1, index + 2)
73 } else if grid[index + cols].is_ascii_uppercase() {
74 (index + cols, index - cols, index + 2 * cols)
75 } else {
76 continue;
77 };
78 let before_open = grid[before] == b'.';
79 let after_open = grid[after] == b'.';
80 if before_open == after_open {
81 return Err(InputError::new(
82 input,
83 0,
84 "expected one open tile next to portal label",
85 ));
86 }
87
88 let entrance = if before_open { before } else { after };
89 if portal_nodes[entrance] != u16::MAX {
90 return Err(InputError::new(
91 input,
92 0,
93 "open tile belongs to multiple portals",
94 ));
95 }
96
97 let label = usize::from(tile - b'A') * 26 + usize::from(grid[second] - b'A');
98 let count = endpoint_count[label] as usize;
99 if matches!(label, AA | ZZ) && count != 0 {
100 return Err(InputError::new(
101 input,
102 0,
103 format!("duplicate {} portal", Self::label_string(label)),
104 ));
105 }
106 if count == 2 {
107 return Err(InputError::new(
108 input,
109 0,
110 format!("more than two {} portals", Self::label_string(label)),
111 ));
112 }
113
114 endpoints[label][count] = entrance as u16;
115 endpoint_count[label] += 1;
116 portal_nodes[entrance] = label as u16;
117 }
118
119 for label in [AA, ZZ] {
120 if endpoint_count[label] == 0 {
121 return Err(InputError::new(
122 input,
123 0,
124 format!("expected one {} portal", Self::label_string(label)),
125 ));
126 }
127 }
128 for (label, &count) in endpoint_count.iter().enumerate().take(ZZ).skip(1) {
129 if count == 1 {
130 return Err(InputError::new(
131 input,
132 0,
133 format!("expected two {} portals", Self::label_string(label)),
134 ));
135 }
136 }
137
138 let mut positions = Vec::new();
140 let mut portals = Vec::new();
141 positions.extend_from_slice(&[endpoints[AA][0], endpoints[ZZ][0]]);
142 portals.extend_from_slice(&[
143 Portal {
144 exit: u16::MAX,
145 outer: false,
146 },
147 Portal {
148 exit: u16::MAX,
149 outer: false,
150 },
151 ]);
152 for label in 1..ZZ {
153 if endpoint_count[label] == 0 {
154 continue;
155 }
156
157 let [first, second] = endpoints[label];
158 let first_node = positions.len() as u16;
159 let second_node = first_node + 1;
160 positions.extend_from_slice(&[first, second]);
161 portals.extend_from_slice(&[
162 Portal {
163 exit: second_node,
164 outer: false,
165 },
166 Portal {
167 exit: first_node,
168 outer: false,
169 },
170 ]);
171 }
172 for (node, &position) in positions.iter().enumerate() {
173 portal_nodes[usize::from(position)] = node as u16;
174 }
175
176 let mut grid_node_ids = vec![u16::MAX; grid.len()];
178 let mut grid_node_positions = Vec::new();
179 let (mut min_row, mut max_row, mut min_col, mut max_col) = (rows, 0, cols, 0);
180 for index in 0..grid.len() {
181 if grid[index] != b'.' {
182 continue;
183 }
184
185 let (row, col) = (index / cols, index % cols);
186 min_row = min_row.min(row);
187 max_row = max_row.max(row);
188 min_col = min_col.min(col);
189 max_col = max_col.max(col);
190
191 let degree = [index - 1, index + 1, index - cols, index + cols]
192 .into_iter()
193 .filter(|&next| grid[next] == b'.')
194 .count();
195 if portal_nodes[index] != u16::MAX || degree != 2 {
196 grid_node_ids[index] = grid_node_positions.len() as u16;
197 grid_node_positions.push(index);
198 }
199 }
200 let mut grid_edges = vec![ArrayVec::<Edge, 4>::new(); grid_node_positions.len()];
201 for (from, &position) in grid_node_positions.iter().enumerate() {
202 for mut next in [position - 1, position + 1, position - cols, position + cols] {
203 if grid[next] != b'.' {
204 continue;
205 }
206
207 let mut previous = position;
208 let mut distance = 1u16;
209 while grid_node_ids[next] == u16::MAX {
210 let advance = [next - 1, next + 1, next - cols, next + cols]
211 .into_iter()
212 .find(|&candidate| candidate != previous && grid[candidate] == b'.')
213 .unwrap();
214 previous = next;
215 next = advance;
216 distance += 1;
217 }
218
219 grid_edges[from]
220 .push(Edge {
221 target: grid_node_ids[next],
222 distance,
223 })
224 .unwrap();
225 }
226 }
227
228 let mut graph = vec![Vec::new(); positions.len()];
230 let mut distances = vec![u16::MAX; grid_node_positions.len()];
231 let mut heap = BinaryHeap::with_capacity(grid_node_positions.len());
233 for (from, &start) in positions.iter().enumerate() {
234 distances.fill(u16::MAX);
235 heap.push(Reverse(u32::from(grid_node_ids[usize::from(start)])));
236
237 while let Some(Reverse(entry)) = heap.pop() {
238 let (distance, node) = ((entry >> 16) as u16, entry as u16 as usize);
239 if distance >= distances[node] {
240 continue;
241 }
242 distances[node] = distance;
243
244 let target = portal_nodes[grid_node_positions[node]];
245 if target != u16::MAX && usize::from(target) != from {
246 graph[from].push(Edge { target, distance });
247 }
248
249 for &edge in &grid_edges[node] {
250 let next_distance = distance.saturating_add(edge.distance);
251 if next_distance < distances[usize::from(edge.target)] {
252 heap.push(Reverse(
253 (u32::from(next_distance) << 16) | u32::from(edge.target),
254 ));
255 }
256 }
257 }
258 }
259
260 for (portal, &position) in portals.iter_mut().zip(&positions) {
262 let position = usize::from(position);
263 let (row, col) = (position / cols, position % cols);
264 portal.outer = row == min_row || row == max_row || col == min_col || col == max_col;
265 }
266
267 Ok(Self { graph, portals })
268 }
269
270 #[must_use]
271 pub fn part1(&self) -> u32 {
272 let mut distances = vec![u16::MAX; self.graph.len()];
273 distances[START] = 0;
274
275 let mut heap = BinaryHeap::new();
277 heap.push(Reverse(START as u32));
278
279 while let Some(Reverse(entry)) = heap.pop() {
280 let (distance, node) = ((entry >> 16) as u16, entry as u16 as usize);
281 if distance != distances[node] {
282 continue;
283 }
284 if node == END {
285 return u32::from(distance);
286 }
287
288 for &edge in &self.graph[node] {
289 let mut target = usize::from(edge.target);
290 if target == START {
291 continue;
292 }
293
294 let mut next_distance = distance + edge.distance;
295 if target != END {
296 target = usize::from(self.portals[target].exit);
297 next_distance += 1;
298 }
299
300 if next_distance < distances[target] {
301 let next_distance = next_distance;
302 distances[target] = next_distance;
303 heap.push(Reverse((u32::from(next_distance) << 16) | (target as u32)));
304 }
305 }
306 }
307
308 panic!("no solution found")
309 }
310
311 #[must_use]
312 pub fn part2(&self) -> u32 {
313 assert!(
314 self.portals[START].outer,
315 "expected AA to be on the outer edge"
316 );
317 assert!(
318 self.portals[END].outer,
319 "expected ZZ to be on the outer edge"
320 );
321 assert!(
322 self.portals[2..]
323 .as_chunks::<2>()
324 .0
325 .iter()
326 .all(|[a, b]| a.outer != b.outer),
327 "expected portal pairs to have one inner and one outer portal"
328 );
329
330 let node_count = self.graph.len();
331
332 let mut distances = vec![u32::MAX; node_count];
333 distances[START] = 0;
334
335 let mut heap = BinaryHeap::new();
337 heap.push(Reverse(START as u64));
338
339 while let Some(Reverse(entry)) = heap.pop() {
340 let (distance, level, node) = (
341 (entry >> 32) as u32,
342 (entry >> 16) as u16 as usize,
343 entry as u16 as usize,
344 );
345 if distance != distances[level * node_count + node] {
346 continue;
347 }
348 if node == END && level == 0 {
349 return distance;
350 }
351
352 for &edge in &self.graph[node] {
353 let mut target = usize::from(edge.target);
354 let mut next_level = level;
355 let mut next_distance = distance + u32::from(edge.distance);
356
357 if target == START {
358 continue;
359 } else if target == END {
360 if level != 0 {
361 continue;
362 }
363 } else {
364 let portal = self.portals[target];
365 if level == 0 && portal.outer {
366 continue;
367 }
368 next_level = if portal.outer { level - 1 } else { level + 1 };
369 target = usize::from(portal.exit);
370 next_distance += 1;
371 }
372
373 let next_index = next_level * node_count + target;
374 if next_index >= distances.len() {
375 distances.resize(distances.len() + node_count, u32::MAX);
376 }
377 if next_distance < distances[next_index] {
378 distances[next_index] = next_distance;
379 heap.push(Reverse(
380 (u64::from(next_distance) << 32)
381 | ((next_level as u64) << 16)
382 | (target as u64),
383 ));
384 }
385 }
386 }
387
388 panic!("no solution found")
389 }
390
391 #[cold]
392 fn label_string(label: usize) -> String {
393 let first = char::from(b'A' + (label / 26) as u8);
394 let second = char::from(b'A' + (label % 26) as u8);
395 format!("{first}{second}")
396 }
397}
398
399examples!(Day20 -> (u32, u32) [
400 {file: "day20_example0.txt", part1: 23},
401 {file: "day20_example1.txt", part1: 58},
402 {file: "day20_example2.txt", part2: 396},
403]);