aoc/year2021/day19.rs
1//! # Beacon Scanner
2//!
3//! A brute force approach is:
4//! * Choose an arbitrary starting scanner, then add its beacons to a "known" set.
5//! * For each remaining scanner, then for each of its possible 24 rotations, check its beacons
6//! by translating against every other beacon in the known set.
7//! * If we find a match of 12 or more overlapping beacons, then merge the beacons into the known
8//! set.
9//!
10//! This approach will work but is a little slow as the number of potential comparisons is quite
11//! high. We can speed things up by first creating a "signature" for each beacon similar to how
12//! a hash is computed for an item in a hash map. Ideally this signature should be the same no
13//! matter what the rotation of the beacons, as this will reduce the number of comparisons by a
14//! factor of 24.
15//!
16//! The set of Euclidean distance squared between all beacons is a good choice, as it's invariant
17//! under rotation and translation, quick to calculate and a good discriminant. To check for an
18//! overlap of 12 beacons, we look for an overlap of a least 12 * 11 / 2 = 66 distances.
19//! (12 beacons gives 12 * 11 = 132 pairs of distances but divided by 2 since the distance from
20//! a -> b is the same as b -> a).
21//!
22//! An overlap indicates a potential match, but we need to confirm by checking the beacons against
23//! each other in two steps. First confirming orientation by matching the deltas between
24//! points, then by translating the beacons until 12 overlap.
25use crate::util::hash::*;
26use crate::util::iter::*;
27use crate::util::parse::*;
28use std::ops::{Add, Sub};
29
30/// Stores coordinates in x, y, z order.
31#[derive(Copy, Clone, Hash, PartialEq, Eq)]
32struct Point3D(i32, i32, i32);
33
34impl Point3D {
35 fn parse([x, y, z]: [i32; 3]) -> Point3D {
36 Point3D(x, y, z)
37 }
38
39 /// There are 24 possible 3D rotations of each point in increments of 90 degrees.
40 fn transform(&self, index: usize) -> Point3D {
41 let Point3D(x, y, z) = *self;
42 match index {
43 0 => Point3D(x, y, z),
44 1 => Point3D(x, z, -y),
45 2 => Point3D(x, -z, y),
46 3 => Point3D(x, -y, -z),
47 4 => Point3D(-x, -z, -y),
48 5 => Point3D(-x, y, -z),
49 6 => Point3D(-x, -y, z),
50 7 => Point3D(-x, z, y),
51 8 => Point3D(y, z, x),
52 9 => Point3D(y, -x, z),
53 10 => Point3D(y, x, -z),
54 11 => Point3D(y, -z, -x),
55 12 => Point3D(-y, x, z),
56 13 => Point3D(-y, z, -x),
57 14 => Point3D(-y, -z, x),
58 15 => Point3D(-y, -x, -z),
59 16 => Point3D(z, x, y),
60 17 => Point3D(z, y, -x),
61 18 => Point3D(z, -y, x),
62 19 => Point3D(z, -x, -y),
63 20 => Point3D(-z, y, x),
64 21 => Point3D(-z, -x, y),
65 22 => Point3D(-z, x, -y),
66 23 => Point3D(-z, -y, -x),
67 _ => unreachable!(),
68 }
69 }
70
71 /// No need to take the square root as it's faster and easier to just use the integer
72 /// value of the distance squared directly.
73 fn euclidean(&self, other: &Point3D) -> i32 {
74 let Point3D(dx, dy, dz) = *self - *other;
75 dx * dx + dy * dy + dz * dz
76 }
77
78 fn manhattan(&self, other: &Point3D) -> i32 {
79 let Point3D(dx, dy, dz) = *self - *other;
80 dx.abs() + dy.abs() + dz.abs()
81 }
82}
83
84/// Implement operators for points so that we can write `a + b` or `a - b`.
85impl Add for Point3D {
86 type Output = Point3D;
87
88 fn add(self, rhs: Point3D) -> Point3D {
89 let Point3D(x1, y1, z1) = self;
90 let Point3D(x2, y2, z2) = rhs;
91 Point3D(x1 + x2, y1 + y2, z1 + z2)
92 }
93}
94
95impl Sub for Point3D {
96 type Output = Point3D;
97
98 fn sub(self, rhs: Point3D) -> Point3D {
99 let Point3D(x1, y1, z1) = self;
100 let Point3D(x2, y2, z2) = rhs;
101 Point3D(x1 - x2, y1 - y2, z1 - z2)
102 }
103}
104
105/// Represents an unknown scanner that could be at any orientation and translation
106/// from our initial reference scanner.
107struct Scanner {
108 beacons: Vec<Point3D>,
109 signature: FastMap<i32, [usize; 2]>,
110}
111
112impl Scanner {
113 /// Calculate the signature as the set of Euclidean distance squared between every possible
114 /// pair of beacons.
115 fn parse(block: &str) -> Scanner {
116 // Each beacon header results in 5 mangled numbers at the start that should be skipped.
117 let beacons: Vec<_> =
118 block.iter_signed().skip(5).chunk::<3>().map(Point3D::parse).collect();
119
120 // Include indices of the points so that we can match translation and rotation for
121 // points that have the same signature. Use indices so that we don't need to recalculate
122 // signature when rotating and translation a beacon from unknown to known.
123 let mut signature = FastMap::with_capacity(1_000);
124 for i in 0..(beacons.len() - 1) {
125 for j in (i + 1)..beacons.len() {
126 let key = beacons[i].euclidean(&beacons[j]);
127 let value = [i, j];
128 signature.insert(key, value);
129 }
130 }
131
132 Scanner { beacons, signature }
133 }
134}
135
136/// Returns the correct orientation and translation to link a new scanner to an existing
137/// reference scanner.
138#[derive(Clone, Copy)]
139struct Found {
140 orientation: usize,
141 translation: Point3D,
142}
143
144/// Represents a known scanner with the same orientation and a known translation from
145/// our initial reference scanner.
146pub struct Located {
147 beacons: Vec<Point3D>,
148 signature: FastMap<i32, [usize; 2]>,
149 oriented: FastSet<Point3D>,
150 translation: Point3D,
151}
152
153impl Located {
154 fn new(scanner: Scanner, found: Found) -> Self {
155 let Scanner { beacons, signature } = scanner;
156 let Found { orientation, translation } = found;
157
158 // Rotate and translate the beacons by the offset of this scanner from the reference, so
159 // that we can build "chains" of scanners, for example A -> B -> C, where A and B overlap,
160 // B and C overlap, but not A and C.
161 let beacons: Vec<_> =
162 beacons.iter().map(|b| b.transform(orientation) + translation).collect();
163 let oriented = beacons.iter().copied().collect();
164
165 Located { beacons, signature, oriented, translation }
166 }
167}
168
169/// Convert the raw input into a vec of unknown scanners, then do all the heavy lifting of figuring
170/// out the relative orientations and translations of each scanner.
171///
172/// First choose an arbitrary scanner that determines the reference orientation and that we
173/// decide is located at the origin.
174///
175/// Then for each remaining unknown scanner, check if the signature indicates a potential
176/// match. If confirmed, we determine the orientation and translation then add the scanner
177/// to a todo list to recheck against other unknown scanners.
178///
179/// This works for situations such as A -> B -> C, where A and B overlap, B and C overlap, but not
180/// A and C.
181pub fn parse(input: &str) -> Vec<Located> {
182 let mut unknown: Vec<_> = input.split("\n\n").map(Scanner::parse).collect();
183 let mut todo = Vec::new();
184 let mut done = Vec::new();
185
186 let scanner = unknown.pop().unwrap();
187 let found = Found { orientation: 0, translation: Point3D(0, 0, 0) };
188 todo.push(Located::new(scanner, found));
189
190 while let Some(known) = todo.pop() {
191 let mut next_unknown = Vec::new();
192
193 while let Some(scanner) = unknown.pop() {
194 match check(&known, &scanner) {
195 Some(found) => todo.push(Located::new(scanner, found)),
196 None => next_unknown.push(scanner),
197 }
198 }
199
200 done.push(known);
201 unknown = next_unknown;
202 }
203
204 done
205}
206
207/// Calculate the total number of distinct beacons.
208pub fn part1(input: &[Located]) -> usize {
209 input.iter().flat_map(|located| &located.beacons).collect::<FastSet<_>>().len()
210}
211
212/// Calculate the maximum manhattan distance between any two scanners.
213pub fn part2(input: &[Located]) -> i32 {
214 let mut result = 0;
215
216 // This solution uses the usual quadratic pairing of every point. This is okay because
217 // the set is not terribly large, and the runtime here is dwarfed by the earlier runtime
218 // taken to get the coordinates in place. However, a linear solution is also possible:
219 // https://www.reddit.com/r/adventofcode/comments/rygnl8/2021_day_19_part_2pseudocode_speeding_up/
220 for first in input {
221 for second in input {
222 result = result.max(first.translation.manhattan(&second.translation));
223 }
224 }
225
226 result
227}
228
229/// At least 66 Euclidean distances must overlap for a potential match.
230fn check(known: &Located, scanner: &Scanner) -> Option<Found> {
231 let mut matching = 0;
232
233 for key in known.signature.keys() {
234 if scanner.signature.contains_key(key) {
235 matching += 1;
236 if matching == 66 {
237 // Choose any arbitrary pair of points that have a matching signature.
238 let [a, b] = known.signature[key];
239 let [x, y] = scanner.signature[key];
240 let points =
241 [known.beacons[a], known.beacons[b], scanner.beacons[x], scanner.beacons[y]];
242 return detailed_check(known, scanner, points);
243 }
244 }
245 }
246
247 None
248}
249
250/// The correct translation and orientation is found when we have at least 12 beacons overlapping.
251fn detailed_check(known: &Located, scanner: &Scanner, points: [Point3D; 4]) -> Option<Found> {
252 let [a, b, x, y] = points;
253 let delta = a - b;
254
255 for orientation in 0..24 {
256 let rotate_x = x.transform(orientation);
257 let rotate_y = y.transform(orientation);
258
259 let translation = if rotate_x - rotate_y == delta {
260 b - rotate_y
261 } else if rotate_y - rotate_x == delta {
262 b - rotate_x
263 } else {
264 continue;
265 };
266
267 let mut count = 0;
268
269 for candidate in &scanner.beacons {
270 let point = candidate.transform(orientation) + translation;
271
272 if known.oriented.contains(&point) {
273 count += 1;
274 if count == 12 {
275 return Some(Found { orientation, translation });
276 }
277 }
278 }
279 }
280
281 None
282}