Skip to main content

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