1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
//! # Conway Cubes
//!
//! Solving this problem reveals an interesting insight that the active cells are very sparse.
//! Of the total possible volume of the cube formed by the maximum extents in part one, only
//! roughly 8% is active and of the hypercube from part two only 3% is active for my input.
//!
//! To speed things up our high level strategy will flip from a "pull" model where we check the
//! surroundings neighbors of each cell, to a "push" model where we update the neighbors of each
//! active cell instead.
//!
//! A `HashSet` is generally a good choice for very sparse infinite grids, however for this
//! problem we'll pack all dimensions into a single `vec` to achieve a five times increase
//! in lookup speed.
use crate::util::grid::*;
use crate::util::point::*;
/// x and y dimensions are in the plane of the input. Each dimension can expand at most two in each
/// axis per round (one positive and one negative). Adding padding at the edges to avoid boundary
/// checks gives a maximum width of 8 + 2 * (6 + 1) = 22 for the x and y dimensions and
/// 1 + 2 * (6 + 1) = 15 for the z and w dimensions.
pub mod size {
pub const X: i32 = 22;
pub const Y: i32 = 22;
pub const Z: i32 = 15;
pub const W: i32 = 15;
}
/// Pack a four dimensional array into a one dimensional vec to avoid the speed penalty of
/// following multiple pointers and increase memory locality for caching.
pub mod stride {
use super::size;
pub const X: i32 = 1;
pub const Y: i32 = size::X * X;
pub const Z: i32 = size::Y * Y;
pub const W: i32 = size::Z * Z;
}
/// Use our utility [`Grid`] method to parse the input.
///
/// [`Grid`]: crate::util::grid::Grid
pub fn parse(input: &str) -> Grid<u8> {
Grid::parse(input)
}
/// Part one cells form a cube.
pub fn part1(input: &Grid<u8>) -> usize {
let size = size::X * size::Y * size::Z;
let base = stride::X + stride::Y + stride::Z;
boot_process(input, size, base, &[0])
}
/// Part two form a hypercube.
pub fn part2(input: &Grid<u8>) -> usize {
let size = size::X * size::Y * size::Z * size::W;
let base = stride::X + stride::Y + stride::Z + stride::W;
boot_process(input, size, base, &[-1, 0, 1])
}
/// Re-use logic between both parts.
fn boot_process(input: &Grid<u8>, size: i32, base: i32, fourth_dimension: &[i32]) -> usize {
let dimension = [-1, 0, 1];
let mut neighbors = Vec::new();
// Pre-calculate either the 26 or 80 offsets formed by the combination of dimensions.
for x in dimension {
for y in dimension {
for z in dimension {
for w in fourth_dimension {
let offset = x * stride::X + y * stride::Y + z * stride::Z + w * stride::W;
if offset != 0 {
neighbors.push(offset as usize);
}
}
}
}
}
let mut active = Vec::with_capacity(5_000);
let mut candidates = Vec::with_capacity(5_000);
let mut next_active = Vec::with_capacity(5_000);
// To prevent negative array indices offset the starting cells by seven units in each
// dimension. This allows six for growth, plus one for padding to prevent needing edge checks.
for x in 0..input.width {
for y in 0..input.height {
if input[Point::new(x, y)] == b'#' {
let index = 7 * base + x + y * stride::Y;
active.push(index as usize);
}
}
}
for _ in 0..6 {
let mut state: Vec<u8> = vec![0; size as usize];
for &cube in &active {
for &offset in &neighbors {
// Earlier we converted the offsets from signed `i32` to unsigned `usize`. To
// achieve subtraction for negative indices, we use a `wrapping_add` that performs
// [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) arithmetic.
let index = cube.wrapping_add(offset);
state[index] += 1;
if state[index] == 3 {
candidates.push(index);
}
}
}
// Active cubes remain active with both two and three neighbors.
for &cube in &active {
if state[cube] == 2 {
next_active.push(cube);
}
}
// Check that the neighbor count for inactive cubes hasn't exceeded three.
for &cube in &candidates {
if state[cube] == 3 {
next_active.push(cube);
}
}
// Swap to make next generation the current generation.
(active, next_active) = (next_active, active);
candidates.clear();
next_active.clear();
}
active.len()
}