aoc/year2020/
day17.rs

1//! # Conway Cubes
2//!
3//! Solving this problem reveals an interesting insight that the active cells are very sparse.
4//! Of the total possible volume of the cube formed by the maximum extents in part one, only
5//! roughly 8% is active and of the hypercube from part two only 3% is active for my input.
6//!
7//! To speed things up our high level strategy will flip from a "pull" model where we check the
8//! surrounding neighbors of each cell, to a "push" model where we update the neighbors of each
9//! active cell instead.
10//!
11//! A `HashSet` is generally a good choice for very sparse infinite grids, however for this
12//! problem we'll pack all dimensions into a single `vec` to achieve a five times increase
13//! in lookup speed.
14use crate::util::grid::*;
15use crate::util::point::*;
16
17/// Use our utility [`Grid`] method to parse the input.
18///
19/// [`Grid`]: crate::util::grid::Grid
20pub fn parse(input: &str) -> Grid<u8> {
21    Grid::parse(input)
22}
23
24/// Part one cells form a cube.
25pub fn part1(input: &Grid<u8>) -> usize {
26    #[cfg(not(feature = "simd"))]
27    let result = scalar::three_dimensions(input);
28
29    #[cfg(feature = "simd")]
30    let result = simd::three_dimensions(input);
31
32    result
33}
34
35/// Part two forms a hypercube.
36pub fn part2(input: &Grid<u8>) -> usize {
37    #[cfg(not(feature = "simd"))]
38    let result = scalar::four_dimensions(input);
39
40    #[cfg(feature = "simd")]
41    let result = simd::four_dimensions(input);
42
43    result
44}
45
46#[cfg(not(feature = "simd"))]
47mod scalar {
48    use super::*;
49
50    /// x and y dimensions are in the plane of the input. Each dimension can expand by at most two
51    /// in each axis per round (one positive and one negative). Adding padding at the edges to avoid
52    /// boundary checks gives a maximum width of 8 + 2 * (6 + 1) = 22 for the x and y dimensions and
53    /// 1 + 2 * (6 + 1) = 15 for the z and w dimensions.
54    const X: i32 = 22;
55    const Y: i32 = 22;
56    const Z: i32 = 15;
57    const W: i32 = 15;
58
59    /// Pack a four dimensional array into a one dimensional vec to avoid the speed penalty of
60    /// following multiple pointers and increase memory locality for caching.
61    const STRIDE_X: i32 = 1;
62    const STRIDE_Y: i32 = X * STRIDE_X;
63    const STRIDE_Z: i32 = Y * STRIDE_Y;
64    const STRIDE_W: i32 = Z * STRIDE_Z;
65
66    pub(super) fn three_dimensions(input: &Grid<u8>) -> usize {
67        let size = X * Y * Z;
68        let base = STRIDE_X + STRIDE_Y + STRIDE_Z;
69        boot_process(input, size, base, &[0])
70    }
71
72    pub(super) fn four_dimensions(input: &Grid<u8>) -> usize {
73        let size = X * Y * Z * W;
74        let base = STRIDE_X + STRIDE_Y + STRIDE_Z + STRIDE_W;
75        boot_process(input, size, base, &[-1, 0, 1])
76    }
77
78    /// Re-use logic between both parts.
79    fn boot_process(input: &Grid<u8>, size: i32, base: i32, fourth_dimension: &[i32]) -> usize {
80        let dimension = [-1, 0, 1];
81        let mut neighbors = Vec::new();
82
83        // Pre-calculate either the 26 or 80 offsets formed by the combination of dimensions.
84        for x in dimension {
85            for y in dimension {
86                for z in dimension {
87                    for w in fourth_dimension {
88                        let offset = x * STRIDE_X + y * STRIDE_Y + z * STRIDE_Z + w * STRIDE_W;
89                        if offset != 0 {
90                            neighbors.push(offset as usize);
91                        }
92                    }
93                }
94            }
95        }
96
97        let mut active = Vec::with_capacity(5_000);
98        let mut candidates = Vec::with_capacity(5_000);
99        let mut next_active = Vec::with_capacity(5_000);
100
101        // To prevent negative array indices offset the starting cells by seven units in each
102        // dimension. This allows six for growth, plus one for padding to prevent needing edge checks.
103        for x in 0..input.width {
104            for y in 0..input.height {
105                if input[Point::new(x, y)] == b'#' {
106                    let index = 7 * base + x + y * STRIDE_Y;
107                    active.push(index as usize);
108                }
109            }
110        }
111
112        for _ in 0..6 {
113            let mut state: Vec<u8> = vec![0; size as usize];
114
115            for &cube in &active {
116                for &offset in &neighbors {
117                    // Earlier we converted the offsets from signed `i32` to unsigned `usize`. To
118                    // achieve subtraction for negative indices, we use a `wrapping_add` that performs
119                    // [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) arithmetic.
120                    let index = cube.wrapping_add(offset);
121                    state[index] += 1;
122
123                    if state[index] == 3 {
124                        candidates.push(index);
125                    }
126                }
127            }
128
129            // Active cubes remain active with both two and three neighbors.
130            for &cube in &active {
131                if state[cube] == 2 {
132                    next_active.push(cube);
133                }
134            }
135
136            // Check that the neighbor count for inactive cubes hasn't exceeded three.
137            for &cube in &candidates {
138                if state[cube] == 3 {
139                    next_active.push(cube);
140                }
141            }
142
143            // Swap to make next generation the current generation.
144            (active, next_active) = (next_active, active);
145            candidates.clear();
146            next_active.clear();
147        }
148
149        active.len()
150    }
151}
152
153#[cfg(feature = "simd")]
154mod simd {
155    use super::*;
156    use std::simd::cmp::SimdPartialEq as _;
157    use std::simd::*;
158
159    const LANE_WIDTH: usize = 32;
160    type Vector = Simd<u8, LANE_WIDTH>;
161
162    #[expect(clippy::needless_range_loop)]
163    pub(super) fn three_dimensions(input: &Grid<u8>) -> usize {
164        // Each dimension can expand by at most two in each axis per round. Adding padding at the
165        // edges to avoid boundary checks gives a maximum width of 8 + 2 * (6 + 1) = 22 for the x
166        // and y dimensions and 1 + 2 * (6 + 1) = 15 for the z and w dimensions.
167        let mut current = [[[0; 32]; 22]; 15];
168        let mut next = current;
169        // Temporary intermediate state.
170        let mut first = current;
171
172        // Set initial cubes offset from the edges to allow for growth.
173        for x in 0..input.width {
174            for y in 0..input.height {
175                if input[Point::new(x, y)] == b'#' {
176                    current[7][y as usize + 7][x as usize + 7] = 1;
177                }
178            }
179        }
180
181        let zero: Vector = Simd::splat(0);
182        let one: Vector = Simd::splat(1);
183        let three: Vector = Simd::splat(3);
184
185        for round in 0..6 {
186            // Each round state boundary expands by 1 in both positive and negative direction.
187            let edge = 5 - round;
188
189            // Sum xs and ys.
190            for z in (1 + edge)..(14 - edge) {
191                for y in (1 + edge)..(21 - edge) {
192                    let above = xs_sum(&current[z][y - 1]);
193                    let row = xs_sum(&current[z][y]);
194                    let below = xs_sum(&current[z][y + 1]);
195
196                    (row + above + below).copy_to_slice(&mut first[z][y]);
197                }
198            }
199
200            // Sum zs and calculate next state.
201            for z in (1 + edge)..(14 - edge) {
202                for y in (1 + edge)..(21 - edge) {
203                    let above = from_slice(&first[z - 1][y]);
204                    let row = from_slice(&first[z][y]);
205                    let below = from_slice(&first[z + 1][y]);
206                    let state = from_slice(&current[z][y]);
207
208                    let total = row + above + below - state;
209                    let result = (state | total).simd_eq(three).select(one, zero);
210                    result.copy_to_slice(&mut next[z][y]);
211                }
212            }
213
214            (current, next) = (next, current);
215        }
216
217        let mut result = 0;
218
219        for z in 1..14 {
220            for y in 1..21 {
221                for x in 1..21 {
222                    result += current[z][y][x] as usize;
223                }
224            }
225        }
226
227        result
228    }
229
230    #[expect(clippy::needless_range_loop)]
231    pub(super) fn four_dimensions(input: &Grid<u8>) -> usize {
232        // Same size logic as part one with added `w` dimension.
233        let mut current = [[[[0; 32]; 22]; 15]; 15];
234        let mut next = current;
235        // Temporary intermediate state.
236        let mut first = current;
237        let mut second = current;
238
239        // Set initial cubes offset from the edges to allow for growth.
240        for x in 0..input.width {
241            for y in 0..input.height {
242                if input[Point::new(x, y)] == b'#' {
243                    current[7][7][y as usize + 7][x as usize + 7] = 1;
244                }
245            }
246        }
247
248        let zero: Vector = Simd::splat(0);
249        let one: Vector = Simd::splat(1);
250        let three: Vector = Simd::splat(3);
251
252        for round in 0..6 {
253            // Each round state boundary expands by 1 in both positive and negative direction.
254            let edge = 5 - round;
255
256            // Sum xs and ys.
257            for w in (1 + edge)..(14 - edge) {
258                for z in (1 + edge)..(14 - edge) {
259                    for y in (1 + edge)..(21 - edge) {
260                        let above = xs_sum(&current[w][z][y - 1]);
261                        let row = xs_sum(&current[w][z][y]);
262                        let below = xs_sum(&current[w][z][y + 1]);
263
264                        (row + above + below).copy_to_slice(&mut first[w][z][y]);
265                    }
266                }
267            }
268
269            // Sum zs.
270            for w in (1 + edge)..(14 - edge) {
271                for z in (1 + edge)..(14 - edge) {
272                    for y in (1 + edge)..(21 - edge) {
273                        let above = from_slice(&first[w][z - 1][y]);
274                        let row = from_slice(&first[w][z][y]);
275                        let below = from_slice(&first[w][z + 1][y]);
276
277                        (row + above + below).copy_to_slice(&mut second[w][z][y]);
278                    }
279                }
280            }
281
282            // Sum ws and calculate next state.
283            for w in (1 + edge)..(14 - edge) {
284                for z in (1 + edge)..(14 - edge) {
285                    for y in (1 + edge)..(21 - edge) {
286                        let above = from_slice(&second[w - 1][z][y]);
287                        let row = from_slice(&second[w][z][y]);
288                        let below = from_slice(&second[w + 1][z][y]);
289                        let state = from_slice(&current[w][z][y]);
290
291                        let total = row + above + below - state;
292                        let result = (state | total).simd_eq(three).select(one, zero);
293                        result.copy_to_slice(&mut next[w][z][y]);
294                    }
295                }
296            }
297
298            (current, next) = (next, current);
299        }
300
301        let mut result = 0;
302
303        for w in 1..14 {
304            for z in 1..14 {
305                for y in 1..21 {
306                    for x in 1..21 {
307                        result += current[w][z][y][x] as usize;
308                    }
309                }
310            }
311        }
312
313        result
314    }
315
316    #[inline]
317    fn from_slice(slice: &[u8]) -> Vector {
318        Simd::from_slice(slice)
319    }
320
321    /// Create SIMD vector of the sum of left, right and center lanes.
322    #[inline]
323    fn xs_sum(slice: &[u8]) -> Vector {
324        let center = Simd::from_slice(slice);
325        let left = center.shift_elements_left::<1>(0);
326        let right = center.shift_elements_right::<1>(0);
327
328        center + left + right
329    }
330}