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