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    pub(super) fn simulate(input: &Grid<u8>, part_two: bool, limit: u8) -> u32 {
40        let mut seats = Vec::new();
41
42        for y in 0..input.height {
43            for x in 0..input.width {
44                let point = Point::new(x, y);
45                if input[point] != SEAT {
46                    continue;
47                }
48
49                let mut size = 0;
50                let mut neighbors = [ORIGIN; 8];
51
52                for direction in DIAGONAL {
53                    let mut next = point + direction;
54
55                    // Part one considers only the adjacent square. Part two skips over any
56                    // floor until reaching the first visible seat or the edge of the grid.
57                    while part_two && input.contains(next) && input[next] != SEAT {
58                        next += direction;
59                    }
60
61                    if input.contains(next) && input[next] == SEAT {
62                        neighbors[size] = next;
63                        size += 1;
64                    }
65                }
66
67                seats.push(Seat { point, size, neighbors });
68            }
69        }
70
71        let mut current = input.same_size_with(0);
72        let mut next = input.same_size_with(0);
73
74        loop {
75            for seat in &seats {
76                let total: u8 = seat.neighbors[..seat.size].iter().map(|&i| current[i]).sum();
77
78                next[seat.point] = if current[seat.point] == 0 {
79                    u8::from(total == 0)
80                } else {
81                    u8::from(total < limit)
82                };
83            }
84
85            (current, next) = (next, current);
86            if current == next {
87                return current.bytes.iter().map(|&n| n as u32).sum();
88            }
89        }
90    }
91}
92
93#[cfg(feature = "simd")]
94mod implementation {
95    use super::*;
96    use std::simd::cmp::SimdPartialEq as _;
97    use std::simd::cmp::SimdPartialOrd as _;
98    use std::simd::*;
99
100    const LANE_WIDTH: usize = 32;
101    type Vector = Simd<u8, LANE_WIDTH>;
102
103    pub(super) fn simulate(input: &Grid<u8>, part_two: bool, limit: u8) -> u32 {
104        // Input grid is taller than it is wide. To make efficient use of the wide SIMD operations:
105        // * Add an empty border to eliminate bounds checking.
106        // * Transpose the input grid to make it wider than it is tall.
107        // * Round width up to next multiple of LANE_WIDTH.
108        let width = 2 + (input.height as usize).next_multiple_of(LANE_WIDTH) as i32;
109        let height = 2 + input.width;
110        let mut grid = Grid::new(width, height, 0);
111
112        for y in 0..input.height {
113            for x in 0..input.width {
114                let from = Point::new(x, y);
115                let to = Point::new(y + 1, x + 1);
116                grid[to] = u8::from(input[from] == SEAT);
117            }
118        }
119
120        // Build a list of seats that are non-adjacent but visible to each other.
121        let mut visible = Vec::new();
122
123        if part_two {
124            for y in 0..height {
125                for x in 0..width {
126                    let from = Point::new(x, y);
127                    if grid[from] == 0 {
128                        continue;
129                    }
130
131                    for direction in DIAGONAL {
132                        if grid[from + direction] == 1 {
133                            continue;
134                        }
135
136                        let mut to = from + direction * 2;
137                        while grid.contains(to) {
138                            if grid[to] == 1 {
139                                visible.push((from, to));
140                                break;
141                            }
142                            to += direction;
143                        }
144                    }
145                }
146            }
147        }
148
149        // Common constants.
150        let zero = Simd::splat(0);
151        let one = Simd::splat(1);
152        let limit = Simd::splat(limit);
153
154        let mut current = grid.same_size_with(0);
155        let mut next = grid.same_size_with(0);
156        let mut extra = grid.same_size_with(0);
157
158        loop {
159            // Add any non-adjacent seats that are visible to the total.
160            if part_two {
161                extra.bytes.fill(0);
162                for &(from, to) in &visible {
163                    extra[to] += current[from];
164                }
165            }
166
167            // Process grid column by column using wide SIMD vectors.
168            for x in (1..width - 1).step_by(LANE_WIDTH) {
169                let mut above = horizontal_neighbors(&current, x, 0);
170                let mut row = horizontal_neighbors(&current, x, 1);
171
172                for y in 1..height - 1 {
173                    let index = (width * y + x) as usize;
174                    let seats = Simd::from_slice(&grid.bytes[index..]);
175                    let occupied = Simd::from_slice(&current.bytes[index..]);
176                    let extra = Simd::from_slice(&extra.bytes[index..]);
177
178                    let below = horizontal_neighbors(&current, x, y + 1);
179                    let total = row + above + below + extra;
180                    above = row;
181                    row = below;
182
183                    // Empty to occupied.
184                    let first = total.simd_eq(zero).select(one, zero);
185                    // Occupied to empty.
186                    let second = total.simd_le(limit).select(occupied, zero);
187                    // Nobody sits on the floor.
188                    let result = (first + second) & seats;
189
190                    result.copy_to_slice(&mut next.bytes[index..]);
191                }
192            }
193
194            (current, next) = (next, current);
195            if current == next {
196                return current.bytes.iter().map(|&b| b as u32).sum();
197            }
198        }
199    }
200
201    /// Create SIMD vector of the sum of left, right and center lanes.
202    #[inline]
203    fn horizontal_neighbors(grid: &Grid<u8>, x: i32, y: i32) -> Vector {
204        let index = (grid.width * y + x) as usize;
205
206        let center = Simd::from_slice(&grid.bytes[index..]);
207        let left = center.shift_elements_left::<1>(grid.bytes[index + LANE_WIDTH]);
208        let right = center.shift_elements_right::<1>(grid.bytes[index - 1]);
209
210        center + left + right
211    }
212}