Skip to content

Commit bd6a22a

Browse files
committed
Year 2019 Day 18
1 parent 2f5d022 commit bd6a22a

File tree

8 files changed

+374
-0
lines changed

8 files changed

+374
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ pie
210210
| 15 | [Oxygen System](https://adventofcode.com/2019/day/15) | [Source](src/year2019/day15.rs) | 442 |
211211
| 16 | [Flawed Frequency Transmission](https://adventofcode.com/2019/day/16) | [Source](src/year2019/day16.rs) | 4124 |
212212
| 17 | [Set and Forget](https://adventofcode.com/2019/day/17) | [Source](src/year2019/day17.rs) | 425 |
213+
| 18 | [Many-Worlds Interpretation](https://adventofcode.com/2019/day/18) | [Source](src/year2019/day18.rs) | 1077 |
213214

214215
## 2015
215216

benches/benchmark.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ mod year2019 {
6666
benchmark!(year2019, day15);
6767
benchmark!(year2019, day16);
6868
benchmark!(year2019, day17);
69+
benchmark!(year2019, day18);
6970
}
7071

7172
mod year2020 {

input/year2019/day18.txt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#################################################################################
2+
#.............#..o..........#...........#...............#b......#...#.........#.#
3+
#.#######.#.#.#.#.#########.###.#######.###.#########.###.###.###.#.#C#####.#.#.#
4+
#.#...A.#.#.#.#.#.#w....#.#...#..e#.....#...#.....#...#...#...#...#.#.....#.#k..#
5+
#.#.###.###.#.###.###.#.#.###.#####.###.#.###.#####.###.###.###.###.#####.#.###.#
6+
#.#...#.....#...#.#...#.....#.....#...#.#...#.#...#.#...#.#.....#.#.....#.#.#...#
7+
#.###.#########.#.#.#######.#####.###.#.###.#.#.#.#.#.###.#######.###.#.#.#.#####
8+
#...#.#...........#.......#.....#.....#.#...#...#.#.#.......#.....#...#.#.#.#...#
9+
###.#.###.#############.#.#############.#.###.###.#P#######.###.#.#.###.#.#.#.#.#
10+
#...#...#.#.....#.....#.#.....#...#.....#.#.....#.......#...#...#.#.#...#.#...#.#
11+
#.#####.#.#.###.#.###.#######.#.#.#.#####.###############.###.#####.#.###.#####.#
12+
#.....#.#.#.#.#.#...#.#.....#.#.#.#.....#.......#.......#.#.......#.#...#.#...#.#
13+
###.###.#.#.#.#.###.#.#.###.#.#.#.#####.#.#####.#.#####.#.#######.#.###.#.#.###.#
14+
#...#...#.#...#.#...#.#.#...#...#.....#.#.#.#...#.#.#...#...#.....#.#...#.#.#...#
15+
#.###.#######.#.#.###.#.###.#.#####.###.#.#.#.#.#.#.#.#####.#.###.#D###.#.#.#.###
16+
#...#.....#...#.....#.#...#.#.#...#.#...#...#.#.#...#.....#.#.#.X.#...#.#...#...#
17+
###.#####.#.#####.###.###.#.###.#.#.#.#######.#.###.#####.#.#.###.###.#####.###.#
18+
#.#.....#...#...#.#...#.#.#.#...#.#.#...#.....#.#.#.#...#.#.#...#...#...#...#...#
19+
#.#####.#####.#.###.#.#.#.#.#.###.#.###.#.#####.#.#.#.#.#.#.###.#.#####.#.###.###
20+
#.#...#.#.....#.....#...#.#...#...#.....#.#.....#.#.#.#.#.......#.#...#.....#...#
21+
#.#.#.#.#.###.#########.#.#####.#######.#.#.#####.#.###.#########.#.#.#########.#
22+
#...#...#...#.#...#...#.#.#...#.......#.#.#.....#.#.#.....#...#...#.#.........#i#
23+
#.#####.###.#.#.#.#.#####.#.#########.#.#.#####.#.#.#.#####.#######.#########.#.#
24+
#.#...#...#.#.#.#...#.....#.........#.#.#.....#...#...#n..#..f..#.Z.#...W...#.#.#
25+
#K#Y#.#.###.#.#.#####.#####.###.#####.#.#####.#####.###.#.#####.#.###.#.#####.#.#
26+
#.#.#.#.#...#.#.#.#...#...#.#.#.#...#.#.#.....#.....#...#.......#.#.#.#......h#.#
27+
#.#.#.###.#.###.#.#.#.###.#.#.#.#.#.#.#.#.#J###.#####.###########.#.#.#########.#
28+
#.#.#.....#.#...#...#...#...#.#.#.#...#.#.#.#...#...#.....#...#g..#.#.#...#.....#
29+
###.#######.#.###.#####.#.###.#.#.#######.###.#####.#####.#.#.#.###.#.#.###.###.#
30+
#...#.....#.#.#...#.....#.....#.#.......#...#.#.......#.#...#...#.....#.....#...#
31+
#.#####.###.#.#.###.#########.#.#######.#.#.#.###.###.#.#########.#####.#####.###
32+
#.....#.....#d#...#.#...#...#.#.......#.#.#.#...#...#.#.......#.#.#...#...#.#...#
33+
#.###.#.#####.###.#.#.#.#.#.#.###.#####.###.###.###.#.###.###.#.#.###.###.#.###.#
34+
#.#...#...#.#.#...#.#.#.#.#...#.#.....#.#...#.#.....#...#.#.#...#...#.....#.#...#
35+
#.#.#####.#.#.#####.#.#.#.#####.#####.#.#.#.#.#########.#.#.###.###.#####.#.#.###
36+
#.#.....#.#.#...#...#.#.#...#.......#.#.#.#.......#...#...#...#...#.....#.#.#...#
37+
#.#####V#.#.###.#.###.#.###.#.#####.#.#.#.#######.#.#.#####.#.###.#####.#.#U###.#
38+
#.....#.#.....#...#...#...#.#.....#.#...#...#.....#.#.......#.#...#..v#.#.....#.#
39+
#####.#.###########.#######.#####.#.#####.#.#######.#####.#####.#####.#.#######.#
40+
#.....#...........................#.......#.............#.........R...#l........#
41+
#######################################.@.#######################################
42+
#.............#......r..#.......#...........#.........#...#.......#.....#.#.....#
43+
#.###########.#######.###.#.###.#.#####.#.###.#######.#.#.#.###.###.#.#.#.#.#.###
44+
#...#.......#.......#.....#.#.#...#...#.#.#...#.....#...#.#...#.....#y#...#.#...#
45+
###.#.###.#.#######.#.#####.#.#######.#.#.#.###.###.#####.###.#######.###.#.###Q#
46+
#...#.#...#.#.......#.#.....#.......#.#.#...#...#...#...#...#...#...#...#.#.#.#.#
47+
#.###.#.#.###.#######.#.#######.###.#.#.#.###.#####.#.#.###.###.#.#.###.#.#.#.#.#
48+
#.#...#.#.#.........#.#...#.....#.....#.#.#...#...#...#.#.#.....#.#.#...#.#...#.#
49+
#.#.###.###.#######.#####.#.#.#########.#.###.#.#.#####.#.#######.#.#.#######.#.#
50+
#q#...#.....#.....#.....#.#.#.#...#.....#...#...#.#...#.........#.#.#.#.......#.#
51+
#.#######.###.###.#####.#.#.###.#.#.#######.#.###.#.#.#########.#.#.#.#.#######.#
52+
#.#.....#...#.#.#.#...#.#.#.#...#.#.....#..j#.#...#.#.....#.....#.#.#.....#.....#
53+
#.#.###.#####.#.#.###.#.#.#.#.###.#####.#.###.#.###.#####.###.###.#.#######.###.#
54+
#.#.#.#...#...#.#.#...#...#.#...#.......#.#.#.#.....#...#...#...#.#.......#...#.#
55+
#.#L#.###.#.###.#.#.#######.###.#########.#.#.#########.###.###.#.#######.###S#.#
56+
#.#...#.#.#.#...#.#.#...#.......#...#...#.#.............#.#.#.#.#.#.....#...#.#.#
57+
#.###.#.#.#.###.#.#.#.#.#.#######.#.#.#.#.#.###########.#.#.#.#.#.###.#.###.#.#.#
58+
#...#...#...#...#.#.#.#.........#.#.#.#.#.#.#.......#.#.#.#.#...#...#.#...#.#.#.#
59+
#.#.###.#####.#.#H#.#.#########.#.#.#N#.#.#.#.#####.#.#.#.#.#######.###.#.#.#.#.#
60+
#.#.#...#.....#.#.#.....#...#...#.#...#.#.#.#.#...#.#.....#.#.....#...#.#.#...#.#
61+
###.#.###.#.#####.#.#####.#.#.###.#####.#.###.#.#.#.#.#####.#.###.###.#.#######.#
62+
#...#...#.#...#...#.#.T...#.#.#.F.#.#...#.....#.#.#.#...#...#.#...#...#...#...#.#
63+
#.#####.#.###.#.###########.###.###.#.#########.#.#.###.#.#.###.###.###.#.#.#.#.#
64+
#.....#.#.#u..#..z........#....t#.G...#.#.......#.#.#...#.#...#.#...#...#...#...#
65+
###.#.#.###.###########.#########.#####.#.#####.#.#.#####.#.###.#.#######.#######
66+
#...#s#...#.....#.....#.........#.#.....#.....#.#.#.....#.#.#...#.......#...#...#
67+
#.###.###.###.#.#.###.#######.###.#.###########.#######.#.#.#.###.#####.#####.#.#
68+
#.#.....#...#.#.....#.......#.....#.....#.....#.......#.#.#.#...#.#...#...#...#.#
69+
#.#########.###############.###########.#.###.#####.###.#.#####.#.#.#####.#.###.#
70+
#.#x......#.#...#...........#...........#.#.........#...#.#.....#...#.....#.#...#
71+
#.#.#####.#I#.#.#.#########.#####.#####.#.###########.###.#.#######.#.#####.#.###
72+
#.#...#...#...#...#.....#.#.#...#...#...#.............#...#.....#.#.#.......#.O.#
73+
#.###.#.#############.#.#.#.#.#.###.#.###.###############.#####.#.#.#######.###.#
74+
#...#.#.....#...#.....#...#.#.#.....#.#.#.#...#...#.....#..m#...#...#.....#.#...#
75+
#.###.###.#.#.#.###.#######.#.#######.#.#.#.#.#.#.#.###.###.#.#######.###.#.#.###
76+
#.#...#.#.#...#.....#.....#...#.....#...#.#.#...#.#...#.#...#.....#...#...#.#...#
77+
#.#M###.#.###########.###.#####.###.###.#.#.#####.###.#.#.#######.#.###.#######.#
78+
#.#...#.#...#..a..#...#.#.....#.#.....#.#p..#...#...#.#.#.#.......#.#...#.....#.#
79+
#.###.#.###.#.###.#.###.###.###.#.#####.#####.#####.#.#.#.#.#######.#.###.###.#.#
80+
#.........#...B.#.........#.....#.......#.............#...#.......E.#.....#....c#
81+
#################################################################################

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ pub mod year2019 {
191191
pub mod day15;
192192
pub mod day16;
193193
pub mod day17;
194+
pub mod day18;
194195
pub mod intcode;
195196
}
196197

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ fn all_solutions() -> Vec<Solution> {
104104
solution!(year2019, day15),
105105
solution!(year2019, day16),
106106
solution!(year2019, day17),
107+
solution!(year2019, day18),
107108
// 2020
108109
solution!(year2020, day01),
109110
solution!(year2020, day02),

src/year2019/day18.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//! # Many-Worlds Interpretation
2+
//!
3+
//! Our high level approach is to simplify the problem into graph path finding. We only
4+
//! ever need to move directly from key to key, so the maze becomes a graph where the nodes are
5+
//! keys and the edge weight is the distance between keys. Doors modify which edges
6+
//! are connected depending on the keys currently possessed.
7+
//!
8+
//! We first find the distance betweeen every pair of keys then run
9+
//! [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) to find the
10+
//! shortest path that visits every node in the graph.
11+
12+
//! The maze is also constructed in such a way to make our life easier:
13+
//! * There is only ever one possible path to each key. We do not need to consider
14+
//! paths of different lengths that need different keys.
15+
//! * As a corollary, if key `b` lies between `a` and `c` then `|ac| = |ab| + |bc|`. This
16+
//! enables a huge optimization that we only need to consider immediate neighbours.
17+
//! If we do not possess key `b` then it never makes sense to skip from `a` to `c` since `b` is
18+
//! along the way. We can model this by treating keys the same as doors. This optimization
19+
//! sped up my solution by a factor of 30.
20+
//!
21+
//! On top of this approach we apply some high level tricks to go faster:
22+
//! * We store previously seen pairs of `(location, keys collected)` to `total distance` in a map.
23+
//! If we are in the same location with the same keys but at a higher cost, then this situation
24+
//! can never be optimal so the solution can be discarded.
25+
//! * When finding the distance between every pair of keys, it's faster to first only find the immediate
26+
//! neighbors of each key using a [Breadth first search](https://en.wikipedia.org/wiki/Breadth-first_search)
27+
//! then run the [Floyd-Warshall algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm)
28+
//! to contruct the rest of the graph. Even though the Floyd-Warshall asymptotic bound of `O(n³)`
29+
//! is higher than the asymptotic bounds of repeated BFS, this was twice as fast in practise
30+
//! for my input.
31+
//!
32+
//! We also apply some low level tricks to go even faster:
33+
//! * The set of remaining keys needed is stored as bits in an `u32`. We can have at most 26 keys
34+
//! so this will always fit. For example needing `a`, `b` and `e` is represented as `10011`.
35+
//! * Robot location is also stored the same way. Robots can only ever be in their initial location
36+
//! or at a key, so this gives a max of 26 + 4 = 30 locations. As a nice bonus this allows
37+
//! part one and part two to share the same code.
38+
//! * For fast lookup of distance between keys, the maze is stored as
39+
//! [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_matrix). `a` is index 0, `b` is
40+
//! index 1 and robots's initial positions are from 26 to 29 inclusive.
41+
//! For example (simplifying by moving robot from index 26 to 2):
42+
//!
43+
//! ```none
44+
//! ######### [0 6 2]
45+
//! #[email protected]# => [6 0 4]
46+
//! ######### [2 4 0]
47+
//! ```
48+
49+
// Disable lints with false positives
50+
#![allow(clippy::needless_range_loop)]
51+
#![allow(clippy::unnecessary_lazy_evaluations)]
52+
53+
use crate::util::grid::*;
54+
use crate::util::hash::*;
55+
use crate::util::heap::*;
56+
use std::collections::VecDeque;
57+
58+
/// `position` and `remaining` are both bitfields. For example a robot at key `d` that needs
59+
/// `b` and `c` would be stored as `position = 1000` and `remaining = 110`.
60+
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
61+
struct State {
62+
position: u32,
63+
remaining: u32,
64+
}
65+
66+
/// `distance` in the edge weight between nodes. `needed` stores any doors in between as a bitfield.
67+
#[derive(Clone, Copy)]
68+
struct Door {
69+
distance: u32,
70+
needed: u32,
71+
}
72+
73+
/// `initial` is the complete set of keys that we need to collect. Will always be binary
74+
/// `11111111111111111111111111` for the real input but fewer for sample data.
75+
///
76+
/// `maze` is the adjacency of distances and doors between each pair of keys and the robots
77+
/// starting locations.
78+
struct Maze {
79+
initial: State,
80+
maze: [[Door; 30]; 30],
81+
}
82+
83+
pub fn parse(input: &str) -> Grid<u8> {
84+
Grid::parse(input)
85+
}
86+
87+
pub fn part1(input: &Grid<u8>) -> u32 {
88+
explore(input.width, &input.bytes)
89+
}
90+
91+
pub fn part2(input: &Grid<u8>) -> u32 {
92+
let mut modified = input.bytes.clone();
93+
let mut patch = |s: &str, offset: i32| {
94+
let middle = (input.width * input.height) / 2;
95+
let index = (middle + offset * input.width - 1) as usize;
96+
modified[index..index + 3].copy_from_slice(s.as_bytes());
97+
};
98+
99+
patch("@#@", -1);
100+
patch("###", 0);
101+
patch("@#@", 1);
102+
103+
explore(input.width, &modified)
104+
}
105+
106+
fn parse_maze(width: i32, bytes: &[u8]) -> Maze {
107+
let mut initial = State::default();
108+
let mut found = Vec::new();
109+
let mut robots = 26;
110+
111+
// Find the location of every key and robot in the maze.
112+
for (i, &b) in bytes.iter().enumerate() {
113+
if let Some(key) = is_key(b) {
114+
initial.remaining |= 1 << key;
115+
found.push((i, key));
116+
}
117+
if b == b'@' {
118+
initial.position |= 1 << robots;
119+
found.push((i, robots));
120+
robots += 1;
121+
}
122+
}
123+
124+
// Start a BFS from each key and robot's location stopping at the nearest neighbor.
125+
// As a minor optimization we re-use the same `todo` and `visited` between each search.
126+
let default = Door { distance: u32::MAX, needed: 0 };
127+
let orthogonal = [1, -1, width, -width].map(|i| i as usize);
128+
129+
let mut maze = [[default; 30]; 30];
130+
let mut visited = vec![usize::MAX; bytes.len()];
131+
let mut todo = VecDeque::new();
132+
133+
for (start, from) in found {
134+
todo.push_front((start, 0, 0));
135+
visited[start] = from;
136+
137+
while let Some((index, distance, mut needed)) = todo.pop_front() {
138+
if let Some(door) = is_door(bytes[index]) {
139+
needed |= 1 << door;
140+
}
141+
142+
if let Some(to) = is_key(bytes[index]) {
143+
if distance > 0 {
144+
// Store the reciprocal edge weight and doors in the adjacency matrix.
145+
maze[from][to] = Door { distance, needed };
146+
maze[to][from] = Door { distance, needed };
147+
// Faster to stop here and use Floyd-Warshall later.
148+
continue;
149+
}
150+
}
151+
152+
for delta in orthogonal {
153+
let next_index = index.wrapping_add(delta);
154+
if bytes[next_index] != b'#' && visited[next_index] != from {
155+
todo.push_back((next_index, distance + 1, needed));
156+
visited[next_index] = from;
157+
}
158+
}
159+
}
160+
}
161+
162+
// Fill in the rest of the graph using the Floyd–Warshal algorithm.
163+
// As a slight twist we also build the list of intervening doors at the same time.
164+
for i in 0..30 {
165+
maze[i][i].distance = 0;
166+
}
167+
168+
for k in 0..30 {
169+
for i in 0..30 {
170+
for j in 0..30 {
171+
let candidate = maze[i][k].distance.saturating_add(maze[k][j].distance);
172+
if maze[i][j].distance > candidate {
173+
maze[i][j].distance = candidate;
174+
// `(1 << k)` is a crucial optimization. By treating intermediate keys like
175+
// doors we speed things up by a factor of 30.
176+
maze[i][j].needed = maze[i][k].needed | (1 << k) | maze[k][j].needed;
177+
}
178+
}
179+
}
180+
}
181+
182+
Maze { initial, maze }
183+
}
184+
185+
fn explore(width: i32, bytes: &[u8]) -> u32 {
186+
let mut todo = MinHeap::with_capacity(5_000);
187+
let mut cache = FastMap::with_capacity(5_000);
188+
189+
let Maze { initial, maze } = parse_maze(width, bytes);
190+
todo.push(0, initial);
191+
192+
while let Some((total, State { position, remaining })) = todo.pop() {
193+
// Finish immediately if no keys left.
194+
// Since we're using Dijkstra this will always be the optimal solution.
195+
if remaining == 0 {
196+
return total;
197+
}
198+
199+
let mut robots = position;
200+
201+
while robots != 0 {
202+
// The set of robots is stored as bits in a `u32` shifted by the index of the location.
203+
let from = robots.trailing_zeros() as usize;
204+
let from_mask = 1 << from;
205+
robots ^= from_mask;
206+
207+
let mut keys = remaining;
208+
209+
while keys != 0 {
210+
// The set of keys still needed is also stored as bits in a `u32` similar as robots.
211+
let to = keys.trailing_zeros() as usize;
212+
let to_mask = 1 << to;
213+
keys ^= to_mask;
214+
215+
let Door { distance, needed } = maze[from][to];
216+
217+
// u32::MAX indicates that two nodes are not connected. Only possible in part two.
218+
if distance != u32::MAX && remaining & needed == 0 {
219+
let next_total = total + distance;
220+
let next_state = State {
221+
position: position ^ from_mask ^ to_mask,
222+
remaining: remaining ^ to_mask,
223+
};
224+
225+
// Memoize previously seen states to eliminate suboptimal states right away.
226+
cache
227+
.entry(next_state)
228+
.and_modify(|e| {
229+
if next_total < *e {
230+
todo.push(next_total, next_state);
231+
*e = next_total;
232+
}
233+
})
234+
.or_insert_with(|| {
235+
todo.push(next_total, next_state);
236+
next_total
237+
});
238+
}
239+
}
240+
}
241+
}
242+
243+
unreachable!()
244+
}
245+
246+
// Convenience functions to find keys and robots
247+
fn is_key(b: u8) -> Option<usize> {
248+
b.is_ascii_lowercase().then(|| (b - b'a') as usize)
249+
}
250+
251+
fn is_door(b: u8) -> Option<usize> {
252+
b.is_ascii_uppercase().then(|| (b - b'A') as usize)
253+
}

tests/test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ mod year2019 {
5959
mod day15_test;
6060
mod day16_test;
6161
mod day17_test;
62+
mod day18_test;
6263
}
6364

6465
mod year2020 {

0 commit comments

Comments
 (0)