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