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 comparisions 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 from(scanner: Scanner, found: Found) -> Located {
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::from(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::from(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    let mut result = FastSet::with_capacity(1_000);
210
211    for located in input {
212        for beacon in &located.beacons {
213            result.insert(beacon);
214        }
215    }
216
217    result.len()
218}
219
220/// Calculate the maximum manhattan distance between any two scanners.
221pub fn part2(input: &[Located]) -> i32 {
222    let mut result = 0;
223
224    for first in input {
225        for second in input {
226            result = result.max(first.translation.manhattan(&second.translation));
227        }
228    }
229
230    result
231}
232
233/// At least 66 Euclidean distances must overlap for a potential match.
234fn check(known: &Located, scanner: &Scanner) -> Option<Found> {
235    let mut matching = 0;
236
237    for key in known.signature.keys() {
238        if scanner.signature.contains_key(key) {
239            matching += 1;
240            if matching == 66 {
241                // Choose any arbitrary pair of points that have a matching signature.
242                let [a, b] = known.signature[key];
243                let [x, y] = scanner.signature[key];
244                let points =
245                    [known.beacons[a], known.beacons[b], scanner.beacons[x], scanner.beacons[y]];
246                return detailed_check(known, scanner, points);
247            }
248        }
249    }
250
251    None
252}
253
254/// The correct translation and orientation is found when we have at least 12 beacons overlapping.
255fn detailed_check(known: &Located, scanner: &Scanner, points: [Point3D; 4]) -> Option<Found> {
256    let [a, b, x, y] = points;
257    let delta = a - b;
258
259    for orientation in 0..24 {
260        let rotate_x = x.transform(orientation);
261        let rotate_y = y.transform(orientation);
262
263        let translation = if rotate_x - rotate_y == delta {
264            b - rotate_y
265        } else if rotate_y - rotate_x == delta {
266            b - rotate_x
267        } else {
268            continue;
269        };
270
271        let mut count = 0;
272
273        for candidate in &scanner.beacons {
274            let point = candidate.transform(orientation) + translation;
275
276            if known.oriented.contains(&point) {
277                count += 1;
278                if count == 12 {
279                    return Some(Found { orientation, translation });
280                }
281            }
282        }
283    }
284
285    None
286}