Skip to main content

aoc/year2019/
day20.rs

1//! # Donut Maze
2//!
3//! The approach to this solution is very similar to [`Day 18`] however parsing the maze
4//! cleanly is quite tricky.
5//!
6//! We first simplify the problem by running a [breadth-first search] from each portal
7//! creating a list of distances between each pair of portals.
8//!
9//! Then a second BFS over this list efficiently solves both parts. For part two we use a cache to
10//! memoize previously seen values. We optimize part two further by not recursing deeper than the
11//! number of portals as this would mean a redundant trip to an already seen portal.
12//!
13//! [`Day 18`]: crate::year2019::day18
14//! [breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
15use std::collections::VecDeque;
16
17use crate::util::grid::*;
18use crate::util::hash::*;
19use crate::util::point::*;
20
21type Key = ((u8, u8), Kind);
22
23#[derive(Clone, Copy, Eq, Hash, PartialEq)]
24pub enum Kind {
25    Inner,
26    Outer,
27    Start,
28    End,
29}
30
31enum Tile {
32    Wall,
33    Open,
34    Portal(Key, Kind),
35}
36
37struct Edge {
38    to: usize,
39    kind: Kind,
40    distance: u32,
41}
42
43pub struct Maze {
44    start: usize,
45    portals: Vec<Vec<Edge>>,
46}
47
48/// Parsing takes two passes. First we find the location of each portal. Then we BFS from each
49/// portal to build a list of distance pairs.
50pub fn parse(input: &str) -> Maze {
51    let grid = Grid::parse(input);
52    let width = grid.width as usize;
53
54    let mut tiles: Vec<_> =
55        grid.bytes.iter().map(|&b| if b == b'.' { Tile::Open } else { Tile::Wall }).collect();
56    let mut map = FastMap::new();
57    let mut found = Vec::new();
58    let mut start = usize::MAX;
59
60    // Find all labels.
61    for y in (1..grid.height - 1).step_by(2) {
62        for x in (1..grid.width - 1).step_by(2) {
63            let point = Point::new(x, y);
64            if !grid[point].is_ascii_uppercase() {
65                continue;
66            }
67
68            // Decode the relative orientation of the label and the portal.
69            let (first, second, third) = if grid[point + UP] == b'.' {
70                (point, point + DOWN, point + UP)
71            } else if grid[point + DOWN] == b'.' {
72                (point + UP, point, point + DOWN)
73            } else if grid[point + LEFT] == b'.' {
74                (point, point + RIGHT, point + LEFT)
75            } else if grid[point + RIGHT] == b'.' {
76                (point + LEFT, point, point + RIGHT)
77            } else {
78                continue;
79            };
80
81            let pair = (grid[first], grid[second]);
82            let index = (grid.width * third.y + third.x) as usize;
83            let inner = 2 < x && x < grid.width - 3 && 2 < y && y < grid.height - 3;
84
85            let (kind, opposite) = if inner {
86                (Kind::Inner, Kind::Outer)
87            } else {
88                match pair {
89                    (b'A', b'A') => {
90                        start = found.len();
91                        (Kind::Start, Kind::Start)
92                    }
93                    (b'Z', b'Z') => (Kind::End, Kind::End),
94                    _ => (Kind::Outer, Kind::Inner),
95                }
96            };
97
98            // `(pair, opposite)` is the key to the linked portal. Start and End map to themselves.
99            tiles[index] = Tile::Portal((pair, opposite), kind);
100            map.insert((pair, kind), found.len());
101            found.push(index);
102        }
103    }
104
105    // BFS from each portal. As a minor optimization we reuse `todo` and `seen`.
106    let mut portals = Vec::new();
107    let mut todo = VecDeque::new();
108    let mut seen = vec![0; tiles.len()];
109
110    for start in found {
111        let mut edges = Vec::new();
112        todo.push_back((start, 0));
113
114        while let Some((index, steps)) = todo.pop_front() {
115            seen[index] = start;
116
117            for next_index in [index + 1, index - 1, index + width, index - width] {
118                let next_steps = steps + 1;
119
120                if seen[next_index] != start {
121                    match tiles[next_index] {
122                        Tile::Wall => (),
123                        Tile::Open => {
124                            todo.push_back((next_index, next_steps));
125                        }
126                        Tile::Portal(key, kind) => {
127                            let to = map[&key];
128                            edges.push(Edge { to, kind, distance: next_steps });
129                        }
130                    }
131                }
132            }
133        }
134
135        portals.push(edges);
136    }
137
138    Maze { start, portals }
139}
140
141/// Straight BFS with no caching or any optimization tricks.
142pub fn part1(input: &Maze) -> u32 {
143    let mut todo = VecDeque::new();
144    todo.push_back((0, input.start));
145
146    while let Some((steps, index)) = todo.pop_front() {
147        for &Edge { to, kind, distance } in &input.portals[index] {
148            let next_steps = steps + distance + 1;
149
150            match kind {
151                Kind::Inner | Kind::Outer => todo.push_back((next_steps, to)),
152                Kind::End => return next_steps - 1,
153                Kind::Start => (),
154            }
155        }
156    }
157
158    unreachable!()
159}
160
161/// BFS with memoization of previously seen states.
162pub fn part2(input: &Maze) -> u32 {
163    let mut cache = FastMap::with_capacity(2_000);
164    let mut todo = VecDeque::new();
165    todo.push_back((0, input.start, 0));
166
167    while let Some((steps, index, level)) = todo.pop_front() {
168        let best = cache.entry((index, level)).or_insert(u32::MAX);
169        if *best <= steps {
170            continue;
171        }
172        *best = steps;
173
174        for &Edge { to, kind, distance } in &input.portals[index] {
175            let next_steps = steps + distance + 1;
176
177            match kind {
178                // No need to recurse further than the number of portals.
179                Kind::Inner if level < input.portals.len() => {
180                    todo.push_back((next_steps, to, level + 1));
181                }
182                Kind::Outer if level > 0 => {
183                    todo.push_back((next_steps, to, level - 1));
184                }
185                Kind::End if level == 0 => {
186                    return next_steps - 1;
187                }
188                _ => (),
189            }
190        }
191    }
192
193    unreachable!()
194}