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