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//! surroundings 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/// x and y dimensions are in the plane of the input. Each dimension can expand at most two in each
18/// axis per round (one positive and one negative). Adding padding at the edges to avoid boundary
19/// checks gives a maximum width of 8 + 2 * (6 + 1) = 22 for the x and y dimensions and
20/// 1 + 2 * (6 + 1) = 15 for the z and w dimensions.
21pub mod size {
22 pub const X: i32 = 22;
23 pub const Y: i32 = 22;
24 pub const Z: i32 = 15;
25 pub const W: i32 = 15;
26}
27
28/// Pack a four dimensional array into a one dimensional vec to avoid the speed penalty of
29/// following multiple pointers and increase memory locality for caching.
30pub mod stride {
31 use super::size;
32 pub const X: i32 = 1;
33 pub const Y: i32 = size::X * X;
34 pub const Z: i32 = size::Y * Y;
35 pub const W: i32 = size::Z * Z;
36}
37
38/// Use our utility [`Grid`] method to parse the input.
39///
40/// [`Grid`]: crate::util::grid::Grid
41pub fn parse(input: &str) -> Grid<u8> {
42 Grid::parse(input)
43}
44
45/// Part one cells form a cube.
46pub fn part1(input: &Grid<u8>) -> usize {
47 let size = size::X * size::Y * size::Z;
48 let base = stride::X + stride::Y + stride::Z;
49 boot_process(input, size, base, &[0])
50}
51
52/// Part two form a hypercube.
53pub fn part2(input: &Grid<u8>) -> usize {
54 let size = size::X * size::Y * size::Z * size::W;
55 let base = stride::X + stride::Y + stride::Z + stride::W;
56 boot_process(input, size, base, &[-1, 0, 1])
57}
58
59/// Re-use logic between both parts.
60fn boot_process(input: &Grid<u8>, size: i32, base: i32, fourth_dimension: &[i32]) -> usize {
61 let dimension = [-1, 0, 1];
62 let mut neighbors = Vec::new();
63
64 // Pre-calculate either the 26 or 80 offsets formed by the combination of dimensions.
65 for x in dimension {
66 for y in dimension {
67 for z in dimension {
68 for w in fourth_dimension {
69 let offset = x * stride::X + y * stride::Y + z * stride::Z + w * stride::W;
70 if offset != 0 {
71 neighbors.push(offset as usize);
72 }
73 }
74 }
75 }
76 }
77
78 let mut active = Vec::with_capacity(5_000);
79 let mut candidates = Vec::with_capacity(5_000);
80 let mut next_active = Vec::with_capacity(5_000);
81
82 // To prevent negative array indices offset the starting cells by seven units in each
83 // dimension. This allows six for growth, plus one for padding to prevent needing edge checks.
84 for x in 0..input.width {
85 for y in 0..input.height {
86 if input[Point::new(x, y)] == b'#' {
87 let index = 7 * base + x + y * stride::Y;
88 active.push(index as usize);
89 }
90 }
91 }
92
93 for _ in 0..6 {
94 let mut state: Vec<u8> = vec![0; size as usize];
95
96 for &cube in &active {
97 for &offset in &neighbors {
98 // Earlier we converted the offsets from signed `i32` to unsigned `usize`. To
99 // achieve subtraction for negative indices, we use a `wrapping_add` that performs
100 // [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) arithmetic.
101 let index = cube.wrapping_add(offset);
102 state[index] += 1;
103
104 if state[index] == 3 {
105 candidates.push(index);
106 }
107 }
108 }
109
110 // Active cubes remain active with both two and three neighbors.
111 for &cube in &active {
112 if state[cube] == 2 {
113 next_active.push(cube);
114 }
115 }
116
117 // Check that the neighbor count for inactive cubes hasn't exceeded three.
118 for &cube in &candidates {
119 if state[cube] == 3 {
120 next_active.push(cube);
121 }
122 }
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}