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