Skip to main content

year2019/
day20.rs

1use std::cmp::Reverse;
2use std::collections::BinaryHeap;
3use utils::array::ArrayVec;
4use utils::grid;
5use utils::prelude::*;
6
7/// Finding the shortest paths through a recursive maze.
8///
9/// Similar to [Day 18](crate::Day18), the key optimization is to precompute distances between
10/// every portal.
11#[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        // Find and validate each portal label and entrance
54        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        // Allocate IDs starting with AA/START and ZZ/END
139        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        // Compute a grid graph with corridors between entrances and junctions as weighted edges
177        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        // Run Dijkstra from each portal entrance to every other entrance
229        let mut graph = vec![Vec::new(); positions.len()];
230        let mut distances = vec![u16::MAX; grid_node_positions.len()];
231        // Pack (distance, node) into an u32 so heap uses faster single integer comparisons
232        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        // Store which portals are on the outer edges
261        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        // Pack (distance, node) into an u32
276        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        // Pack (distance, level, node) into an u64
336        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]);