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}