Skip to main content

year2019/
day18.rs

1use std::cmp::Reverse;
2use std::collections::BinaryHeap;
3use utils::array::ArrayVec;
4use utils::bit::BitIterator;
5use utils::grid;
6use utils::hash::FastMap;
7use utils::prelude::*;
8
9/// Finding the shortest path to collect every key.
10///
11/// The key optimization is to precompute the distance between all entrances and keys, along with
12/// the keys required to travel each path. This allows the final search to only track each robot's
13/// position and which keys remain.
14#[derive(Clone, Debug)]
15pub struct Day18 {
16    grid: Vec<u8>,
17    cols: usize,
18    starts: Vec<usize>,
19    key_positions: [Option<usize>; MAX_KEYS],
20    all_keys: u32,
21}
22
23#[derive(Clone, Copy, Debug, Default)]
24struct Edge {
25    distance: u32,
26    needed_mask: u32,
27    target_node_bit: u32,
28    target_key_bit: u32,
29}
30
31const MAX_KEYS: usize = 26;
32const MAX_ROBOTS: usize = 4;
33const MAX_NODES: usize = MAX_KEYS + MAX_ROBOTS;
34
35impl Day18 {
36    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
37        let mut starts = Vec::new();
38        let mut key_positions = [None; MAX_KEYS];
39        let mut all_keys = 0;
40        let mut all_doors = 0;
41        let (_, cols, grid) = grid::parse(
42            input,
43            1,
44            b'#',
45            |b| b,
46            |b| matches!(b, b'#' | b'.'),
47            |i, b| match b {
48                b'@' => {
49                    starts.push(i);
50                    Ok(b'@')
51                }
52                b'a'..=b'z' if key_positions[(b - b'a') as usize].is_some() => Err("duplicate key"),
53                b'a'..=b'z' => {
54                    key_positions[(b - b'a') as usize] = Some(i);
55                    all_keys |= 1 << (b - b'a');
56                    Ok(b)
57                }
58                b'A'..=b'Z' => {
59                    all_doors |= 1 << (b - b'A');
60                    Ok(b)
61                }
62                _ => Err("expected '#', '.', '@' or letter"),
63            },
64        )?;
65        if grid.len() > usize::from(u16::MAX) {
66            return Err(InputError::new(input, 0, "grid too large"));
67        }
68        if starts.len() != 1 && starts.len() != 4 {
69            return Err(InputError::new(input, 0, "expected 1 or 4 entrances"));
70        }
71        if all_doors & !all_keys != 0 {
72            return Err(InputError::new(input, 0, "door without matching key"));
73        }
74
75        Ok(Self {
76            grid,
77            cols,
78            starts,
79            key_positions,
80            all_keys,
81        })
82    }
83
84    #[must_use]
85    pub fn part1(&self) -> u32 {
86        assert_eq!(self.starts.len(), 1, "part 1 requires a single entrance");
87        self.minimum_steps(&self.grid, &self.starts)
88    }
89
90    #[must_use]
91    pub fn part2(&self) -> u32 {
92        if self.starts.len() == 4 {
93            return self.minimum_steps(&self.grid, &self.starts);
94        }
95
96        let mut grid = self.grid.clone();
97        let middle = self.starts[0];
98        let cols = self.cols as isize;
99        for dy in [-cols, 0, cols] {
100            for dx in [-1, 0, 1] {
101                let i = middle.wrapping_add_signed(dy + dx);
102                if grid[i] != (if dy == 0 && dx == 0 { b'@' } else { b'.' }) {
103                    panic!("expected empty spaces around the entrance");
104                }
105                grid[i] = b'#';
106            }
107        }
108
109        let starts = [
110            middle.wrapping_add_signed(-cols - 1),
111            middle.wrapping_add_signed(-cols + 1),
112            middle.wrapping_add_signed(cols - 1),
113            middle.wrapping_add_signed(cols + 1),
114        ];
115        for i in starts {
116            grid[i] = b'@';
117        }
118
119        self.minimum_steps(&grid, &starts)
120    }
121
122    fn minimum_steps(&self, grid: &[u8], starts: &[usize]) -> u32 {
123        let graph = self.build_key_graph(grid, starts);
124        let positions = (1u32 << starts.len()) - 1;
125        let mut cache = FastMap::with_capacity(8192);
126        let result = Self::search(&graph, positions, self.all_keys, &mut cache);
127        if result == u32::MAX {
128            panic!("no solution found");
129        }
130        result
131    }
132
133    fn build_key_graph(&self, grid: &[u8], starts: &[usize]) -> Vec<ArrayVec<Edge, MAX_KEYS>> {
134        let cols = self.cols;
135        let key_count = self.all_keys.count_ones() as usize;
136        let mut key_to_node = [u8::MAX; MAX_KEYS];
137        let mut key_nodes = Vec::with_capacity(key_count);
138        let mut node_positions = Vec::with_capacity(starts.len() + key_count);
139        let mut node_key_bits = Vec::with_capacity(starts.len() + key_count);
140        node_positions.extend_from_slice(starts);
141        node_key_bits.resize(starts.len(), 0);
142
143        for (key, position) in self.key_positions.iter().enumerate() {
144            if let Some(position) = *position {
145                let node = node_positions.len() as u8;
146                key_to_node[key] = node;
147                key_nodes.push((key, node));
148                node_positions.push(position);
149                node_key_bits.push(1 << key);
150            }
151        }
152
153        // Precompute a grid graph with corridors as weighted edges, so each later Dijkstra
154        // search pushes the next grid node instead of every neighboring tile.
155        let mut grid_node_ids = vec![u32::MAX; grid.len()];
156        let mut grid_node_positions = Vec::new();
157        for index in 0..grid.len() {
158            if grid[index] == b'#' {
159                continue;
160            }
161
162            let mut degree = 0;
163            for next in [index - 1, index + 1, index - cols, index + cols] {
164                degree += u32::from(grid[next] != b'#');
165            }
166
167            if grid[index] != b'.' || degree != 2 {
168                grid_node_ids[index] = grid_node_positions.len() as u32;
169                grid_node_positions.push(index);
170            }
171        }
172
173        let mut grid_edges = vec![ArrayVec::<(u32, u16), 4>::new(); grid_node_positions.len()];
174        for (from, &index) in grid_node_positions.iter().enumerate() {
175            for mut next in [index - 1, index + 1, index - cols, index + cols] {
176                if grid[next] == b'#' {
177                    continue;
178                }
179
180                let mut previous = index;
181                let mut edge_dist = 1u16;
182                while grid_node_ids[next] == u32::MAX {
183                    let mut advance = next;
184                    for candidate in [next - 1, next + 1, next - cols, next + cols] {
185                        if candidate != previous && grid[candidate] != b'#' {
186                            advance = candidate;
187                            break;
188                        }
189                    }
190                    previous = next;
191                    next = advance;
192                    edge_dist += 1;
193                }
194
195                grid_edges[from]
196                    .push((grid_node_ids[next], edge_dist))
197                    .expect("expected at most four edges per maze node");
198            }
199        }
200
201        // Run Dijkstra from each start/key to record distances and doors to each next key.
202        let node_count = node_positions.len();
203        let mut dist_matrix = [[u16::MAX; MAX_NODES]; MAX_NODES];
204        let mut required_keys = [[0u32; MAX_NODES]; MAX_NODES];
205        let mut heap = BinaryHeap::with_capacity(grid_node_positions.len());
206        let mut best_dist = vec![u16::MAX; grid_node_positions.len()];
207        for (from, &start_index) in node_positions.iter().enumerate() {
208            best_dist.fill(u16::MAX);
209            heap.push(Reverse((0u16, grid_node_ids[start_index] as usize, 0u32)));
210
211            while let Some(Reverse((distance, grid_node, mut required_doors))) = heap.pop() {
212                if distance >= best_dist[grid_node] {
213                    continue;
214                }
215                best_dist[grid_node] = distance;
216
217                let tile = grid[grid_node_positions[grid_node]];
218                if tile.is_ascii_uppercase() {
219                    required_doors |= 1 << (tile - b'A');
220                }
221
222                if tile.is_ascii_lowercase() && distance != 0 {
223                    let to = key_to_node[(tile - b'a') as usize] as usize;
224                    dist_matrix[from][to] = distance;
225                    dist_matrix[to][from] = distance;
226                    required_keys[from][to] = required_doors;
227                    required_keys[to][from] = required_doors;
228                    continue;
229                }
230
231                for &(next, edge_dist) in &grid_edges[grid_node] {
232                    let next = next as usize;
233                    let next_distance = distance.saturating_add(edge_dist);
234                    if next_distance < best_dist[next] {
235                        heap.push(Reverse((next_distance, next, required_doors)));
236                    }
237                }
238            }
239        }
240
241        for (i, row) in dist_matrix.iter_mut().enumerate().take(node_count) {
242            row[i] = 0;
243        }
244
245        // Add paths through other keys using Floyd-Warshall
246        for k in 0..node_count {
247            let via_key = node_key_bits[k];
248            for i in 0..node_count {
249                for j in 0..node_count {
250                    let candidate = dist_matrix[i][k].saturating_add(dist_matrix[k][j]);
251                    if dist_matrix[i][j] > candidate {
252                        dist_matrix[i][j] = candidate;
253                        required_keys[i][j] = required_keys[i][k] | via_key | required_keys[k][j];
254                    }
255                }
256            }
257        }
258
259        let mut graph = Vec::with_capacity(node_count);
260        for from in 0..node_count {
261            let mut edges = ArrayVec::new();
262            for &(key, target_node) in &key_nodes {
263                if target_node as usize == from {
264                    continue;
265                }
266
267                let distance = dist_matrix[from][target_node as usize];
268                if distance != u16::MAX {
269                    edges
270                        .push(Edge {
271                            distance: u32::from(distance),
272                            needed_mask: required_keys[from][target_node as usize] | (1 << key),
273                            target_node_bit: 1u32 << target_node,
274                            target_key_bit: 1 << key,
275                        })
276                        .expect("expected at most MAX_KEYS target key edges");
277                }
278            }
279            graph.push(edges);
280        }
281
282        graph
283    }
284
285    fn search(
286        graph: &[ArrayVec<Edge, MAX_KEYS>],
287        positions: u32,
288        remaining: u32,
289        cache: &mut FastMap<(u32, u32), u32>,
290    ) -> u32 {
291        if remaining == 0 {
292            return 0;
293        }
294
295        let key = (positions, remaining);
296        if let Some(&cached) = cache.get(&key) {
297            return cached;
298        }
299
300        let mut best = u32::MAX;
301        for (from, from_bit) in BitIterator::ones(positions) {
302            for &edge in &graph[from as usize] {
303                // needed_mask includes the target key, so one comparison checks every needed
304                // key has been collected and the target key is still remaining
305                if remaining & edge.needed_mask == edge.target_key_bit {
306                    let rest = Self::search(
307                        graph,
308                        positions ^ from_bit ^ edge.target_node_bit,
309                        remaining ^ edge.target_key_bit,
310                        cache,
311                    );
312                    if rest != u32::MAX {
313                        best = best.min(edge.distance + rest);
314                    }
315                }
316            }
317        }
318
319        cache.insert(key, best);
320        best
321    }
322}
323
324examples!(Day18 -> (u32, u32) [
325    {file: "day18_example0.txt", part1: 8},
326    {file: "day18_example1.txt", part1: 86},
327    {file: "day18_example2.txt", part1: 132},
328    {file: "day18_example3.txt", part1: 136},
329    {file: "day18_example4.txt", part1: 81},
330    {file: "day18_example5.txt", part2: 8},
331    {file: "day18_example6.txt", part2: 24},
332    {file: "day18_example7.txt", part2: 32},
333    {file: "day18_example8.txt", part2: 72},
334]);