Skip to main content

aoc/year2020/
day11.rs

1//! # Seating System
2//!
3//! Cellular automata are hard to speed up due to the need to check all neighbors each iteration.
4//! For both parts we minimize expensive memory allocation by creating only two temporary buffers
5//! then swapping between them each turn, a similar approach to double buffering.
6//!
7//! For part two we can further optimize by precalculating the locations of the nearest visible
8//! seats only once then reusing that information for each step.
9//!
10//! The SIMD version speeds things up by calculating 32 lanes at a time.
11use crate::util::grid::*;
12use crate::util::point::*;
13use implementation::*;
14
15const SEAT: u8 = b'L';
16
17pub fn parse(input: &str) -> Grid<u8> {
18    Grid::parse(input)
19}
20
21pub fn part1(input: &Grid<u8>) -> u32 {
22    simulate(input, false, 4)
23}
24
25pub fn part2(input: &Grid<u8>) -> u32 {
26    simulate(input, true, 5)
27}
28
29#[cfg(not(feature = "simd"))]
30mod implementation {
31    use super::*;
32
33    struct Seat {
34        point: Point,
35        size: usize,
36        neighbors: [Point; 8],
37    }
38
39    impl Seat {
40        fn push(&mut self, index: Point) {
41            self.neighbors[self.size] = index;
42            self.size += 1;
43        }
44    }
45
46    pub(super) fn simulate(input: &Grid<u8>, part_two: bool, limit: u8) -> u32 {
47        let mut seats = Vec::new();
48
49        for y in 0..input.height {
50            for x in 0..input.width {
51                let point = Point::new(x, y);
52                if input[point] != SEAT {
53                    continue;
54                }
55
56                let mut seat = Seat { point, size: 0, neighbors: [ORIGIN; 8] };
57
58                for direction in DIAGONAL {
59                    if part_two {
60                        let mut next = point + direction;
61                        while input.contains(next) {
62                            if input[next] == SEAT {
63                                seat.push(next);
64                                break;
65                            }
66                            next += direction;
67                        }
68                    } else {
69                        let next = point + direction;
70                        if input.contains(next) && input[next] == SEAT {
71                            seat.push(next);
72                        }
73                    }
74                }
75
76                seats.push(seat);
77            }
78        }
79
80        let mut current = input.same_size_with(0);
81        let mut next = input.same_size_with(0);
82
83        loop {
84            for seat in &seats {
85                let total: u8 = seat.neighbors[0..seat.size].iter().map(|&i| current[i]).sum();
86
87                next[seat.point] = if current[seat.point] == 0 {
88                    u8::from(total == 0)
89                } else {
90                    u8::from(total < limit)
91                };
92            }
93
94            (current, next) = (next, current);
95            if current == next {
96                return current.bytes.iter().map(|&n| n as u32).sum();
97            }
98        }
99    }
100}
101
102#[cfg(feature = "simd")]
103mod implementation {
104    use super::*;
105    use std::simd::cmp::SimdPartialEq as _;
106    use std::simd::cmp::SimdPartialOrd as _;
107    use std::simd::*;
108
109    const LANE_WIDTH: usize = 32;
110    type Vector = Simd<u8, LANE_WIDTH>;
111
112    pub(super) fn simulate(input: &Grid<u8>, part_two: bool, limit: u8) -> u32 {
113        // Input grid is taller than it is wide. To make efficient use of the wide SIMD operations:
114        // * Add an empty border to eliminate bounds checking.
115        // * Transpose the input grid to make it wider than it is tall.
116        // * Round width up to next multiple of LANE_WIDTH.
117        let width = 2 + (input.height as usize).next_multiple_of(LANE_WIDTH) as i32;
118        let height = 2 + input.width;
119        let mut grid = Grid::new(width, height, 0);
120
121        for y in 0..input.height {
122            for x in 0..input.width {
123                let from = Point::new(x, y);
124                let to = Point::new(y + 1, x + 1);
125                grid[to] = u8::from(input[from] == SEAT);
126            }
127        }
128
129        // Build a list of seats that are non-adjacent but visible to each other.
130        let mut visible = Vec::new();
131
132        if part_two {
133            for y in 0..height {
134                for x in 0..width {
135                    let from = Point::new(x, y);
136                    if grid[from] == 0 {
137                        continue;
138                    }
139
140                    for direction in DIAGONAL {
141                        if grid[from + direction] == 1 {
142                            continue;
143                        }
144
145                        let mut to = from + direction * 2;
146                        while grid.contains(to) {
147                            if grid[to] == 1 {
148                                visible.push((from, to));
149                                break;
150                            }
151                            to += direction;
152                        }
153                    }
154                }
155            }
156        }
157
158        // Common constants.
159        let zero = Simd::splat(0);
160        let one = Simd::splat(1);
161        let limit = Simd::splat(limit);
162
163        let mut current = grid.same_size_with(0);
164        let mut next = grid.same_size_with(0);
165        let mut extra = grid.same_size_with(0);
166
167        loop {
168            // Add any non-adjacent seats that are visible to the total.
169            if part_two {
170                extra.bytes.fill(0);
171                for &(from, to) in &visible {
172                    extra[to] += current[from];
173                }
174            }
175
176            // Process grid column by column using wide SIMD vectors.
177            for x in (1..width - 1).step_by(LANE_WIDTH) {
178                let mut above = horizontal_neighbors(&current, x, 0);
179                let mut row = horizontal_neighbors(&current, x, 1);
180
181                for y in 1..height - 1 {
182                    let index = (width * y + x) as usize;
183                    let seats = Simd::from_slice(&grid.bytes[index..]);
184                    let occupied = Simd::from_slice(&current.bytes[index..]);
185                    let extra = Simd::from_slice(&extra.bytes[index..]);
186
187                    let below = horizontal_neighbors(&current, x, y + 1);
188                    let total = row + above + below + extra;
189                    above = row;
190                    row = below;
191
192                    // Empty to occupied.
193                    let first = total.simd_eq(zero).select(one, zero);
194                    // Occupied to empty.
195                    let second = total.simd_le(limit).select(occupied, zero);
196                    // Nobody sits on the floor.
197                    let result = (first + second) & seats;
198
199                    result.copy_to_slice(&mut next.bytes[index..]);
200                }
201            }
202
203            (current, next) = (next, current);
204            if current == next {
205                return current.bytes.iter().map(|&b| b as u32).sum();
206            }
207        }
208    }
209
210    /// Create SIMD vector of the sum of left, right and center lanes.
211    #[inline]
212    fn horizontal_neighbors(grid: &Grid<u8>, x: i32, y: i32) -> Vector {
213        let index = (grid.width * y + x) as usize;
214
215        let center = Simd::from_slice(&grid.bytes[index..]);
216        let left = center.shift_elements_left::<1>(grid.bytes[index + LANE_WIDTH]);
217        let right = center.shift_elements_right::<1>(grid.bytes[index - 1]);
218
219        center + left + right
220    }
221}