Skip to main content

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 by computing 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⁴ 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, which 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.iter_signed().chunk::<4>().map(|[x, y, z, w]| flatten(x, y, z, w, 11) as usize).collect()
28}
29
30pub fn part1(input: &[usize]) -> usize {
31    let mut constellations = 0;
32    let mut todo = Vec::with_capacity(input.len());
33
34    // Populate a hashtable of all points that still need a constellation.
35    let mut remaining = [false; 23_usize.pow(4)];
36    input.iter().for_each(|&point| remaining[point] = true);
37
38    // Build up the list of interesting offsets.
39    let mut offsets = Vec::with_capacity(128);
40    for x in -3_i32..4 {
41        let lim_y = 3 - x.abs();
42        for y in -lim_y..=lim_y {
43            let lim_z = lim_y - y.abs();
44            for z in -lim_z..=lim_z {
45                let lim_w = lim_z - z.abs();
46                for w in -lim_w..=lim_w {
47                    if x != 0 || y != 0 || z != 0 || w != 0 {
48                        offsets.push(flatten(x, y, z, w, 0) as isize);
49                    }
50                }
51            }
52        }
53    }
54    assert_eq!(offsets.len(), 128);
55
56    // Choose arbitrary point and start a new constellation.
57    for &point in input {
58        if !remaining[point] {
59            continue; // Already part of a constellation
60        }
61        constellations += 1;
62        todo.push(point);
63        remaining[point] = false;
64
65        while let Some(index) = todo.pop() {
66            // Find all neighbors, adding them to `todo` in order to transitively find all
67            // other points in the constellation.
68            for &offset in &offsets {
69                let candidate = index.wrapping_add_signed(offset);
70                if remaining[candidate] {
71                    todo.push(candidate);
72                    remaining[candidate] = false;
73                }
74            }
75        }
76    }
77
78    constellations
79}
80
81pub fn part2(_input: &[usize]) -> &'static str {
82    "n/a"
83}
84
85// Flatten a point into a 1D base-23 number relative to an offset.
86fn flatten(x: i32, y: i32, z: i32, w: i32, offset: i32) -> i32 {
87    [x, y, z, w].into_iter().fold(0, |acc, v| acc * 23 + v + offset)
88}