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 at 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]) -> Self {
36 Self(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) -> Self {
41 let Self(x, y, z) = self;
42 match index {
43 0 => Self(x, y, z),
44 1 => Self(x, z, -y),
45 2 => Self(x, -z, y),
46 3 => Self(x, -y, -z),
47 4 => Self(-x, -z, -y),
48 5 => Self(-x, y, -z),
49 6 => Self(-x, -y, z),
50 7 => Self(-x, z, y),
51 8 => Self(y, z, x),
52 9 => Self(y, -x, z),
53 10 => Self(y, x, -z),
54 11 => Self(y, -z, -x),
55 12 => Self(-y, x, z),
56 13 => Self(-y, z, -x),
57 14 => Self(-y, -z, x),
58 15 => Self(-y, -x, -z),
59 16 => Self(z, x, y),
60 17 => Self(z, y, -x),
61 18 => Self(z, -y, x),
62 19 => Self(z, -x, -y),
63 20 => Self(-z, y, x),
64 21 => Self(-z, -x, y),
65 22 => Self(-z, x, -y),
66 23 => Self(-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: Self) -> i32 {
74 let Self(dx, dy, dz) = self - other;
75 dx * dx + dy * dy + dz * dz
76 }
77
78 fn manhattan(self, other: Self) -> i32 {
79 let Self(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 = Self;
87
88 fn add(self, rhs: Self) -> Self {
89 let Self(x1, y1, z1) = self;
90 let Self(x2, y2, z2) = rhs;
91 Self(x1 + x2, y1 + y2, z1 + z2)
92 }
93}
94
95impl Sub for Point3D {
96 type Output = Self;
97
98 fn sub(self, rhs: Self) -> Self {
99 let Self(x1, y1, z1) = self;
100 let Self(x2, y2, z2) = rhs;
101 Self(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) -> Self {
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 translating 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 signature.insert(beacons[i].euclidean(beacons[j]), [i, j]);
127 }
128 }
129
130 Self { beacons, signature }
131 }
132}
133
134/// Returns the correct orientation and translation to link a new scanner to an existing
135/// reference scanner.
136#[derive(Clone, Copy)]
137struct Found {
138 orientation: usize,
139 translation: Point3D,
140}
141
142/// Represents a known scanner with the same orientation and a known translation from
143/// our initial reference scanner.
144pub struct Located {
145 beacons: Vec<Point3D>,
146 signature: FastMap<i32, [usize; 2]>,
147 oriented: FastSet<Point3D>,
148 translation: Point3D,
149}
150
151impl Located {
152 fn new(scanner: Scanner, found: Found) -> Self {
153 let Scanner { beacons, signature } = scanner;
154 let Found { orientation, translation } = found;
155
156 // Rotate and translate the beacons by the offset of this scanner from the reference, so
157 // that we can build "chains" of scanners, for example A -> B -> C, where A and B overlap,
158 // B and C overlap, but not A and C.
159 let beacons: Vec<_> =
160 beacons.iter().map(|b| b.transform(orientation) + translation).collect();
161 let oriented = beacons.iter().copied().collect();
162
163 Self { beacons, signature, oriented, translation }
164 }
165}
166
167/// Convert the raw input into a vec of unknown scanners, then do all the heavy lifting of figuring
168/// out the relative orientations and translations of each scanner.
169///
170/// First choose an arbitrary scanner that determines the reference orientation and that we
171/// decide is located at the origin.
172///
173/// Then for each remaining unknown scanner, check if the signature indicates a potential
174/// match. If confirmed, we determine the orientation and translation then add the scanner
175/// to a todo list to recheck against other unknown scanners.
176///
177/// This works for situations such as A -> B -> C, where A and B overlap, B and C overlap, but not
178/// A and C.
179pub fn parse(input: &str) -> Vec<Located> {
180 let mut unknown: Vec<_> = input.split("\n\n").map(Scanner::parse).collect();
181 let mut todo = Vec::new();
182 let mut done = Vec::new();
183
184 let scanner = unknown.pop().unwrap();
185 let found = Found { orientation: 0, translation: Point3D(0, 0, 0) };
186 todo.push(Located::new(scanner, found));
187
188 while let Some(known) = todo.pop() {
189 let mut next_unknown = Vec::new();
190
191 while let Some(scanner) = unknown.pop() {
192 match check(&known, &scanner) {
193 Some(found) => todo.push(Located::new(scanner, found)),
194 None => next_unknown.push(scanner),
195 }
196 }
197
198 done.push(known);
199 unknown = next_unknown;
200 }
201
202 done
203}
204
205/// Calculate the total number of distinct beacons.
206pub fn part1(input: &[Located]) -> usize {
207 input.iter().flat_map(|located| &located.beacons).collect::<FastSet<_>>().len()
208}
209
210/// Calculate the maximum Manhattan distance between any two scanners.
211pub fn part2(input: &[Located]) -> i32 {
212 // This solution uses the usual quadratic pairing of every point. This is okay because
213 // the set is not terribly large, and the runtime here is dwarfed by the earlier runtime
214 // taken to get the coordinates in place. However, a linear solution is also possible:
215 // https://www.reddit.com/r/adventofcode/comments/rygnl8/2021_day_19_part_2pseudocode_speeding_up/
216 input
217 .iter()
218 .flat_map(|a| input.iter().map(|b| a.translation.manhattan(b.translation)))
219 .max()
220 .unwrap()
221}
222
223/// At least 66 Euclidean distances must overlap for a potential match.
224fn check(known: &Located, scanner: &Scanner) -> Option<Found> {
225 let mut matching = 0;
226
227 for key in known.signature.keys() {
228 if scanner.signature.contains_key(key) {
229 matching += 1;
230 if matching == 66 {
231 // Choose any arbitrary pair of points that have a matching signature.
232 let [a, b] = known.signature[key];
233 let [x, y] = scanner.signature[key];
234 let points =
235 [known.beacons[a], known.beacons[b], scanner.beacons[x], scanner.beacons[y]];
236 return detailed_check(known, scanner, points);
237 }
238 }
239 }
240
241 None
242}
243
244/// The correct translation and orientation is found when we have at least 12 beacons overlapping.
245fn detailed_check(known: &Located, scanner: &Scanner, points: [Point3D; 4]) -> Option<Found> {
246 let [a, b, x, y] = points;
247 let delta = a - b;
248
249 for orientation in 0..24 {
250 let rotate_x = x.transform(orientation);
251 let rotate_y = y.transform(orientation);
252
253 let translation = if rotate_x - rotate_y == delta {
254 b - rotate_y
255 } else if rotate_y - rotate_x == delta {
256 b - rotate_x
257 } else {
258 continue;
259 };
260
261 let count = scanner
262 .beacons
263 .iter()
264 .filter(|beacon| {
265 known.oriented.contains(&(beacon.transform(orientation) + translation))
266 })
267 .take(12)
268 .count();
269
270 if count == 12 {
271 return Some(Found { orientation, translation });
272 }
273 }
274
275 None
276}