aoc/year2018/day25.rs
1//! # Four-Dimensional Adventure
2//!
3//! This problem is the classic [union find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure).
4//! However since we only need the *count* of the distinct sets we can use a much simpler approach.
5//!
6//! Starting with an arbitrary point we find all other points within range, adding them to a
7//! todo list. We then transitively determine the neighbors of those points, and so on until
8//! all sets have been found.
9//!
10//! The simplest way to check neighbors is to do a quadratic search: compute a distance for
11//! every point compared against every other remaining point. But for our input files ranging
12//! from 1000 to 1300 points, (n²+n)/2 requires well over half a million distance computations.
13//! Inspecting the input file, there are no digits outside [-8,8] in any of the four dimensions;
14//! and even when you consider points looking at neighbors up to distance three away, that still
15//! means we are never probing any dimension outside the range [-11,11]. And 23^4 is merely
16//! 279841 points, which is easy enough to express in memory as a flat 1D array or via a
17//! `FastSet`. What's more, there are exactly 128 points in a 3x3x3x3 hypersphere surrounding
18//! any given point; this is small enough to translate into a lookup table of 128 offsets to
19//! a 1D location to see if another point resides at that offset. With that in hand, we
20//! can now complete the nearest neighbor search with 128*n existence checks, which easily beats
21//! (n²+n)/2 Manhattan distance computations for inputs with more than 1000 points.
22use crate::util::iter::*;
23use crate::util::parse::*;
24
25pub fn parse(input: &str) -> Vec<usize> {
26 // Collapse inputs into a single positive base-23 number offset from -11,-11,-11,-11
27 input
28 .iter_signed::<i32>()
29 .chunk::<4>()
30 .map(|[x, y, z, w]| flatten(x, y, z, w, 11) as usize)
31 .collect()
32}
33
34pub fn part1(input: &[usize]) -> usize {
35 let mut constellations = 0;
36 let mut todo = Vec::with_capacity(input.len());
37
38 // Populate a hashtable of all points that still need a constellation.
39 let mut remaining = [false; 23_usize.pow(4)];
40 input.iter().for_each(|&point| remaining[point] = true);
41
42 // Build up the list of interesting offsets.
43 let mut offsets = Vec::with_capacity(128);
44 for x in -3_i32..4 {
45 let lim_y = 3 - x.abs();
46 for y in -lim_y..lim_y + 1 {
47 let lim_z = lim_y - y.abs();
48 for z in -lim_z..lim_z + 1 {
49 let lim_w = lim_z - z.abs();
50 for w in -lim_w..lim_w + 1 {
51 if x != 0 || y != 0 || z != 0 || w != 0 {
52 offsets.push(flatten(x, y, z, w, 0) as isize);
53 }
54 }
55 }
56 }
57 }
58 assert_eq!(offsets.len(), 128);
59
60 // Choose arbitrary point and start a new constellation.
61 for &point in input {
62 if !remaining[point] {
63 continue; // Already part of a constellation
64 }
65 constellations += 1;
66 todo.push(point);
67 remaining[point] = false;
68
69 while let Some(index) = todo.pop() {
70 // Find all neighbors, adding them to `todo` in order to transitively find all
71 // other points in the constellation.
72 for &offset in &offsets {
73 let candidate = index.wrapping_add_signed(offset);
74 if remaining[candidate] {
75 todo.push(candidate);
76 remaining[candidate] = false;
77 }
78 }
79 }
80 }
81
82 constellations
83}
84
85pub fn part2(_input: &[usize]) -> &'static str {
86 "n/a"
87}
88
89// Flatten a point into a 1D base-23 number relative to an offset.
90fn flatten(x: i32, y: i32, z: i32, w: i32, offset: i32) -> i32 {
91 (x + offset) * 23 * 23 * 23 + (y + offset) * 23 * 23 + (z + offset) * 23 + (w + offset)
92}