Skip to main content

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}