Skip to main content

aoc/year2021/
day20.rs

1//! # Trench Map
2//!
3//! This is a cellular automata problem, similar to Conway's Game of Life, except that the rules
4//! are encoded in the enhancement algorithm string, instead of being statically specified. Each
5//! round the initial square area of cells expands by at most one in each direction, so we can store
6//! the cell in a fixed-size array with enough space on either side to expand into.
7//!
8//! The interesting nuance is handling the edge cells when all 9 cells are empty (index 0) or all
9//! 9 cells are active (index 511). The sample data encodes a blank cell in both scenarios.
10//! My input encoded an active cell for index 0 and a blank cell for index 511, meaning that each
11//! turn the edge cells toggle from set to unset.
12//!
13//! The algorithm keeps track of the bounds of the expanding square and supplies a `default` value,
14//! that in the example case is always zero, but in the real data toggles between zero and one.
15//!
16//! A faster SIMD approach processes cells 16 at a time.
17use crate::util::grid::*;
18use crate::util::point::*;
19use implementation::*;
20
21type Input = (Vec<u8>, Grid<u8>);
22
23pub fn parse(input: &str) -> Input {
24    let (prefix, suffix) = input.split_once("\n\n").unwrap();
25
26    let algorithm = prefix.bytes().map(|b| u8::from(b == b'#')).collect();
27    let grid = Grid::parse(suffix);
28
29    (algorithm, grid)
30}
31
32pub fn part1(input: &Input) -> u32 {
33    enhance(input, 2)
34}
35
36pub fn part2(input: &Input) -> u32 {
37    enhance(input, 50)
38}
39
40#[cfg(not(feature = "simd"))]
41mod implementation {
42    use super::*;
43
44    pub(super) fn enhance(input: &Input, steps: i32) -> u32 {
45        let (algorithm, grid) = input;
46
47        // Offset the initial square by `steps` + 1 buffer cells in both dimensions.
48        // The square expands by at most one in each step so this is enough room to stay within bounds.
49        let extra = steps + 1;
50        let offset = Point::new(extra, extra);
51        let mut pixels = Grid::new(grid.width + 2 * extra, grid.height + 2 * extra, 0);
52
53        for y in 0..grid.height {
54            for x in 0..grid.width {
55                let point = Point::new(x, y);
56                pixels[point + offset] = u8::from(grid[point] == b'#');
57            }
58        }
59
60        let mut next = pixels.clone();
61        let mut default = 0;
62
63        for step in 0..steps {
64            // Boundaries expand by one each turn.
65            let start = extra - step;
66            let end = extra + grid.width + step;
67
68            for y in (start - 1)..(end + 1) {
69                // If the pixel is within current bounds then return it, or else use the `default`
70                // edge value specified by the enhancement algorithm.
71                let helper = |sx, sy, shift| {
72                    let result = if sx < end && start <= sy && sy < end {
73                        pixels[Point::new(sx, sy)]
74                    } else {
75                        default
76                    };
77                    (result as usize) << shift
78                };
79
80                // If the edge pixels are 1 then the initial edge will look like
81                // [##a]
82                // [##b]
83                // [##c]
84                // or 11a11b11c when encoded as an index.
85                let mut index = if default == 1 { 0b11011011 } else { 0b00000000 };
86
87                for x in (start - 1)..(end + 1) {
88                    // Keeps a sliding window of the index, updated as we evaluate the row from
89                    // left to right. Shift the index left by one each turn, updating the values from
90                    // the three new rightmost pixels entering the window.
91                    index = ((index << 1) & 0b110110110)
92                        + helper(x + 1, y - 1, 6)
93                        + helper(x + 1, y, 3)
94                        + helper(x + 1, y + 1, 0);
95
96                    next[Point::new(x, y)] = algorithm[index];
97                }
98            }
99
100            // Swap grids then calculate the next value for edge pixels beyond the boundary.
101            (pixels, next) = (next, pixels);
102            default = if default == 0 { algorithm[0] } else { algorithm[511] };
103        }
104
105        pixels.bytes.iter().map(|&b| b as u32).sum()
106    }
107}
108
109#[cfg(feature = "simd")]
110mod implementation {
111    use super::*;
112    use std::simd::Simd;
113    use std::simd::num::SimdUint as _;
114
115    const LANE_WIDTH: usize = 16;
116    type Vector = Simd<u16, LANE_WIDTH>;
117
118    pub(super) fn enhance(input: &Input, steps: i32) -> u32 {
119        let (algorithm, grid) = input;
120
121        // Offset the initial square by `steps` + 1 buffer cells in both dimensions.
122        // The square expands by at most one in each step so this is enough room to stay within bounds.
123        let extra = steps + 1;
124        let offset = Point::new(extra, extra);
125        let mut pixels =
126            Grid::new(grid.width + 2 * extra + LANE_WIDTH as i32, grid.height + 2 * extra, 0);
127
128        for y in 0..grid.height {
129            for x in 0..grid.width {
130                let point = Point::new(x, y);
131                pixels[point + offset] = u8::from(grid[point] == b'#');
132            }
133        }
134
135        let mut next = pixels.clone();
136        let mut default = 0;
137
138        for step in 0..steps {
139            // Boundaries expand by one each turn.
140            let start = extra - 1 - step;
141            let end = extra + grid.width + 1 + step;
142
143            // Edge pixels on the infinite grid flip-flop between on and off.
144            for y in (start - 1)..(end + 1) {
145                pixels[Point::new(start - 1, y)] = default;
146                pixels[Point::new(start, y)] = default;
147                pixels[Point::new(end - 1, y)] = default;
148                pixels[Point::new(end, y)] = default;
149            }
150
151            for x in (start..end).step_by(LANE_WIDTH) {
152                let edge = Simd::splat(if default == 0 { 0b000 } else { 0b111 });
153                let mut above = edge;
154                let mut row = edge;
155
156                for y in start..end {
157                    let below = if y < end - 2 { from_grid(&pixels, x, y + 1) } else { edge };
158
159                    let indices = (above << 6) | (row << 3) | below;
160                    above = row;
161                    row = below;
162
163                    let base = (pixels.width * y + x) as usize;
164                    for (i, j) in indices.to_array().into_iter().enumerate() {
165                        next.bytes[base + i] = algorithm[j as usize];
166                    }
167                }
168            }
169
170            // Swap grids then calculate the next value for edge pixels beyond the boundary.
171            (pixels, next) = (next, pixels);
172            default = if default == 0 { algorithm[0] } else { algorithm[511] };
173        }
174
175        // Only count pixels inside the boundary.
176        let end = extra + grid.width + 1 + steps;
177        let mut result = 0;
178
179        for y in 1..end - 1 {
180            for x in 1..end - 1 {
181                result += pixels[Point::new(x, y)] as u32;
182            }
183        }
184
185        result
186    }
187
188    #[inline]
189    fn from_grid(grid: &Grid<u8>, x: i32, y: i32) -> Vector {
190        let index = (grid.width * y + x) as usize;
191
192        let row = Simd::from_slice(&grid.bytes[index..]);
193        let left = row.shift_elements_right::<1>(grid[Point::new(x - 1, y)]);
194        let right = row.shift_elements_left::<1>(grid[Point::new(x + LANE_WIDTH as i32, y)]);
195
196        let result = (left << 2) | (row << 1) | right;
197        result.cast()
198    }
199}