Skip to main content

aoc/year2022/
day23.rs

1//! # Unstable Diffusion
2//!
3//! We represent elves as bits in an integer then use bitwise operations to efficiently figure
4//! out the movement for multiple elves at once.
5use Direction::*;
6use implementation::U256;
7
8/// The initial grid is 70 x 70. Elves stop moving when no other elf is adjacent so the grid
9/// will expand at most 70 in any direction, giving 70 + 70 + 70 = 210 total.
10const HEIGHT: usize = 210;
11
12enum Direction {
13    North,
14    South,
15    West,
16    East,
17}
18
19#[derive(Clone, Copy)]
20pub struct Input {
21    grid: [U256; HEIGHT],
22    north: [U256; HEIGHT],
23    south: [U256; HEIGHT],
24    west: [U256; HEIGHT],
25    east: [U256; HEIGHT],
26}
27
28/// Converts the ASCII grid into a bit per elf.
29pub fn parse(input: &str) -> Input {
30    // Enough buffer so that elves won't overflow the edges of the grid.
31    let offset = 70;
32    let raw: Vec<_> = input.lines().map(str::as_bytes).collect();
33    let default = [U256::default(); HEIGHT];
34    let mut grid = default;
35
36    for (y, row) in raw.iter().enumerate() {
37        for (x, col) in row.iter().enumerate() {
38            if *col == b'#' {
39                grid[offset + y].set_bit(offset + x);
40            }
41        }
42    }
43
44    Input { grid, north: default, south: default, west: default, east: default }
45}
46
47pub fn part1(input: &Input) -> usize {
48    let mut input = *input;
49    let mut order = [North, South, West, East];
50
51    for _ in 0..10 {
52        step(&mut input, &mut order);
53    }
54
55    // Find the total number of elves and the bounding rectangle.
56    let grid = input.grid;
57    let elves = grid.iter().flat_map(U256::as_array).map(u8::count_ones).sum::<u32>() as usize;
58
59    // Vertical bounds.
60    let min_y = grid.iter().position(U256::non_zero).unwrap();
61    let max_y = grid.iter().rposition(U256::non_zero).unwrap();
62
63    // Horizontal bounds.
64    let array = grid.iter().fold(U256::default(), |acc, &n| acc.or(n)).as_array();
65    let left = array.iter().position(|&e| e != 0).unwrap();
66    let right = array.iter().rposition(|&e| e != 0).unwrap();
67
68    let min_x = 8 * left + array[left].leading_zeros() as usize;
69    let max_x = 8 * right + (7 - array[right].trailing_zeros()) as usize;
70
71    // Empty ground tiles.
72    (max_x - min_x + 1) * (max_y - min_y + 1) - elves
73}
74
75pub fn part2(input: &Input) -> u32 {
76    let mut input = *input;
77    let mut order = [North, South, West, East];
78    let mut moved = true;
79    let mut count = 0;
80
81    while moved {
82        moved = step(&mut input, &mut order);
83        count += 1;
84    }
85
86    count
87}
88
89fn step(input: &mut Input, order: &mut [Direction]) -> bool {
90    let Input { grid, north, south, west, east } = input;
91    // Optimization to avoid processing empty rows.
92    let start = grid.iter().position(U256::non_zero).unwrap() - 1;
93    let end = grid.iter().rposition(U256::non_zero).unwrap() + 2;
94
95    let mut moved = false;
96
97    let mut prev;
98    // Find horizontal neighbors in each row. To make movement calculations easier
99    // we invert so that a 1 bit means movement is *possible*.
100    let mut cur = grid[0].shr().or(grid[0]).or(grid[0].shl()).not();
101    let mut next = grid[1].shr().or(grid[1]).or(grid[1].shl()).not();
102
103    for i in start..end {
104        // Calculating neighbors is relatively expensive so reuse results between rows.
105        prev = cur;
106        cur = next;
107        next = grid[i + 1].shr().or(grid[i + 1]).or(grid[i + 1].shl()).not();
108
109        let mut up = prev;
110        let mut down = next;
111        // Find neighbors in vertical columns.
112        let vertical = grid[i - 1].or(grid[i]).or(grid[i + 1]).not();
113        let mut left = vertical.shr();
114        let mut right = vertical.shl();
115        // Elves need at least 1 neighbor to propose moving.
116        let mut remaining = grid[i].and(up.and(down).and(left).and(right).not());
117
118        // Consider each direction one at a time, removing any elves who propose it.
119        for direction in &*order {
120            match direction {
121                North => {
122                    up = up.and(remaining);
123                    remaining = remaining.and(up.not());
124                }
125                South => {
126                    down = down.and(remaining);
127                    remaining = remaining.and(down.not());
128                }
129                West => {
130                    left = left.and(remaining);
131                    remaining = remaining.and(left.not());
132                }
133                East => {
134                    right = right.and(remaining);
135                    remaining = remaining.and(right.not());
136                }
137            }
138        }
139
140        // Copy final proposals to an array for each direction.
141        north[i - 1] = up;
142        south[i + 1] = down;
143        west[i] = left.shl();
144        east[i] = right.shr();
145    }
146
147    // Elves that propose moving to the same spot cancel each other out and no-one moves.
148    // Due to the movement rules we only need to check horizontal and vertical movement into
149    // the same spot (horizontal and vertical movement can never collide with each other).
150    for i in start..end {
151        let up = north[i];
152        let down = south[i];
153        let left = west[i];
154        let right = east[i];
155        north[i] = north[i].and(down.not());
156        south[i] = south[i].and(up.not());
157        west[i] = west[i].and(right.not());
158        east[i] = east[i].and(left.not());
159    }
160
161    for i in start..end {
162        // Stationary elves.
163        let same =
164            grid[i].and(north[i - 1].or(south[i + 1]).or(west[i].shr()).or(east[i].shl()).not());
165        // Moving elves.
166        let change = north[i].or(south[i]).or(west[i]).or(east[i]);
167        grid[i] = same.or(change);
168        moved |= change.non_zero();
169    }
170
171    // Rotate the order of movement proposals for the next turn.
172    order.rotate_left(1);
173    moved
174}
175
176#[cfg(not(feature = "simd"))]
177mod implementation {
178    /// Duct tape two `u128`s together.
179    #[derive(Clone, Copy, Default)]
180    pub(super) struct U256 {
181        left: u128,
182        right: u128,
183    }
184
185    impl U256 {
186        pub(super) fn set_bit(&mut self, offset: usize) {
187            if offset < 128 {
188                self.left |= 1 << (127 - offset);
189            } else {
190                self.right |= 1 << (255 - offset);
191            }
192        }
193
194        pub(super) fn as_array(&self) -> [u8; 32] {
195            let mut result = [0; 32];
196            result[..16].copy_from_slice(&self.left.to_be_bytes());
197            result[16..].copy_from_slice(&self.right.to_be_bytes());
198            result
199        }
200
201        pub(super) fn non_zero(&self) -> bool {
202            self.left != 0 || self.right != 0
203        }
204
205        pub(super) fn shl(self) -> U256 {
206            U256 { left: (self.left << 1) | (self.right >> 127), right: (self.right << 1) }
207        }
208
209        pub(super) fn shr(self) -> U256 {
210            U256 { left: (self.left >> 1), right: (self.left << 127) | (self.right >> 1) }
211        }
212
213        pub(super) fn and(self, rhs: U256) -> U256 {
214            U256 { left: self.left & rhs.left, right: self.right & rhs.right }
215        }
216
217        pub(super) fn or(self, rhs: U256) -> U256 {
218            U256 { left: self.left | rhs.left, right: self.right | rhs.right }
219        }
220
221        pub(super) fn not(self) -> U256 {
222            U256 { left: !self.left, right: !self.right }
223        }
224    }
225}
226
227#[cfg(feature = "simd")]
228mod implementation {
229    use std::simd::*;
230
231    #[derive(Clone, Copy, Default)]
232    pub(super) struct U256 {
233        v: Simd<u8, 32>,
234    }
235
236    impl U256 {
237        pub(super) fn set_bit(&mut self, offset: usize) {
238            self.v[offset / 8] |= 1 << (7 - offset % 8);
239        }
240
241        pub(super) fn as_array(&self) -> [u8; 32] {
242            self.v.to_array()
243        }
244
245        pub(super) fn non_zero(&self) -> bool {
246            self.v != Simd::splat(0)
247        }
248
249        pub(super) fn shl(self) -> U256 {
250            U256 { v: (self.v << 1) | (self.v.shift_elements_left::<1>(0) >> 7) }
251        }
252
253        pub(super) fn shr(self) -> U256 {
254            U256 { v: (self.v >> 1) | (self.v.shift_elements_right::<1>(0) << 7) }
255        }
256
257        pub(super) fn and(self, rhs: U256) -> U256 {
258            U256 { v: self.v & rhs.v }
259        }
260
261        pub(super) fn or(self, rhs: U256) -> U256 {
262            U256 { v: self.v | rhs.v }
263        }
264
265        pub(super) fn not(self) -> U256 {
266            U256 { v: !self.v }
267        }
268    }
269}