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