aoc/year2023/
day16.rs

1//! # The Floor Will Be Lava
2//!
3//! Brute force solution tracing the path of each beam, changing direction or splitting
4//! according to the rules of each tile.
5//!
6//! To speed things up the next coordinate in each direction is precomputed for every point
7//! so that the empty spaces between mirrros and splitters are filled efficiently.
8//!
9//! Some beams can enter a closed loop so we keep track of previously seen `(position, direction)`
10//! pairs and stop if we've seen a pair before.
11//!
12//! For part two each path is independent so we can use multiple threads in parallel to speed
13//! up the search.
14use crate::util::grid::*;
15use crate::util::point::*;
16use crate::util::thread::*;
17use std::collections::VecDeque;
18use std::sync::Mutex;
19use std::sync::atomic::{AtomicUsize, Ordering};
20
21type Pair = (Point, u32);
22
23const UP: u32 = 0;
24const DOWN: u32 = 1;
25const LEFT: u32 = 2;
26const RIGHT: u32 = 3;
27
28pub struct Input {
29    grid: Grid<u8>,
30    up: Grid<i32>,
31    down: Grid<i32>,
32    left: Grid<i32>,
33    right: Grid<i32>,
34}
35
36/// Atomics can be safely shared between threads.
37struct Shared<'a> {
38    input: &'a Input,
39    tiles: AtomicUsize,
40    mutex: Mutex<Vec<Pair>>,
41}
42
43/// Build four precomputed grids of the next coordinate in each direction for every point.
44pub fn parse(input: &str) -> Input {
45    let grid = Grid::parse(input);
46
47    let mut up: Grid<i32> = grid.same_size_with(0);
48    let mut down: Grid<i32> = grid.same_size_with(0);
49    let mut left: Grid<i32> = grid.same_size_with(0);
50    let mut right: Grid<i32> = grid.same_size_with(0);
51
52    for x in 0..grid.width {
53        let mut last = -1;
54
55        for y in 0..grid.height {
56            let point = Point::new(x, y);
57            up[point] = last;
58
59            if matches!(grid[point], b'/' | b'\\' | b'-') {
60                last = y;
61            }
62        }
63    }
64
65    for x in 0..grid.width {
66        let mut last = grid.height;
67
68        for y in (0..grid.height).rev() {
69            let point = Point::new(x, y);
70            down[point] = last;
71
72            if matches!(grid[point], b'/' | b'\\' | b'-') {
73                last = y;
74            }
75        }
76    }
77
78    for y in 0..grid.height {
79        let mut last = -1;
80
81        for x in 0..grid.width {
82            let point = Point::new(x, y);
83            left[point] = last;
84
85            if matches!(grid[point], b'/' | b'\\' | b'|') {
86                last = x;
87            }
88        }
89    }
90
91    for y in 0..grid.height {
92        let mut last = grid.width;
93
94        for x in (0..grid.width).rev() {
95            let point = Point::new(x, y);
96            right[point] = last;
97
98            if matches!(grid[point], b'/' | b'\\' | b'|') {
99                last = x;
100            }
101        }
102    }
103
104    Input { grid, up, down, left, right }
105}
106
107pub fn part1(input: &Input) -> usize {
108    count(input, (ORIGIN, RIGHT))
109}
110
111pub fn part2(input: &Input) -> usize {
112    // Build list of edge tiles and directions to process.
113    let Input { grid, .. } = input;
114    let mut todo = Vec::new();
115
116    for x in 0..grid.width {
117        todo.push((Point::new(x, 0), DOWN));
118        todo.push((Point::new(x, grid.height - 1), UP));
119    }
120
121    for y in 0..grid.height {
122        todo.push((Point::new(0, y), RIGHT));
123        todo.push((Point::new(grid.width - 1, y), LEFT));
124    }
125
126    // Setup thread safe wrappers
127    let shared = Shared { input, tiles: AtomicUsize::new(0), mutex: Mutex::new(todo) };
128
129    // Use as many cores as possible to parallelize the search.
130    spawn(|| worker(&shared));
131
132    shared.tiles.load(Ordering::Relaxed)
133}
134
135/// Process starting locations from a shared queue.
136fn worker(shared: &Shared<'_>) {
137    loop {
138        let start = {
139            let mut exclusive = shared.mutex.lock().unwrap();
140            let Some(start) = exclusive.pop() else {
141                break;
142            };
143            start
144        };
145        shared.tiles.fetch_max(count(shared.input, start), Ordering::Relaxed);
146    }
147}
148
149/// Count the number of energized tiles from a single starting location.
150fn count(input: &Input, start: Pair) -> usize {
151    let Input { grid, up, down, left, right } = input;
152
153    let mut todo = VecDeque::with_capacity(1_000);
154    let mut seen: Grid<u8> = grid.same_size_with(0);
155    let mut energized: Grid<bool> = grid.same_size_with(false);
156
157    todo.push_back(start);
158
159    while let Some((position, direction)) = todo.pop_front() {
160        let mut next = |direction: u32| {
161            // Beams can loop, so check if we've already been here.
162            let mask = 1 << direction;
163            if seen[position] & mask != 0 {
164                return;
165            }
166            seen[position] |= mask;
167
168            match direction {
169                UP => {
170                    let x = position.x;
171                    let last = up[position];
172
173                    for y in last..position.y {
174                        energized[Point::new(x, y + 1)] = true;
175                    }
176                    if last >= 0 {
177                        todo.push_back((Point::new(x, last), UP));
178                    }
179                }
180                DOWN => {
181                    let x = position.x;
182                    let last = down[position];
183
184                    for y in position.y..last {
185                        energized[Point::new(x, y)] = true;
186                    }
187                    if last < grid.height {
188                        todo.push_back((Point::new(x, last), DOWN));
189                    }
190                }
191                LEFT => {
192                    let y = position.y;
193                    let last = left[position];
194
195                    for x in last..position.x {
196                        energized[Point::new(x + 1, y)] = true;
197                    }
198                    if last >= 0 {
199                        todo.push_back((Point::new(last, y), LEFT));
200                    }
201                }
202                RIGHT => {
203                    let y = position.y;
204                    let last = right[position];
205
206                    for x in position.x..last {
207                        energized[Point::new(x, y)] = true;
208                    }
209                    if last < grid.width {
210                        todo.push_back((Point::new(last, y), RIGHT));
211                    }
212                }
213                _ => unreachable!(),
214            }
215        };
216
217        match grid[position] {
218            b'.' => next(direction),
219            b'/' => match direction {
220                UP => next(RIGHT),
221                DOWN => next(LEFT),
222                LEFT => next(DOWN),
223                RIGHT => next(UP),
224                _ => unreachable!(),
225            },
226            b'\\' => match direction {
227                UP => next(LEFT),
228                DOWN => next(RIGHT),
229                LEFT => next(UP),
230                RIGHT => next(DOWN),
231                _ => unreachable!(),
232            },
233            b'|' => match direction {
234                UP | DOWN => next(direction),
235                LEFT | RIGHT => {
236                    next(UP);
237                    next(DOWN);
238                }
239                _ => unreachable!(),
240            },
241            b'-' => match direction {
242                LEFT | RIGHT => next(direction),
243                UP | DOWN => {
244                    next(LEFT);
245                    next(RIGHT);
246                }
247                _ => unreachable!(),
248            },
249            _ => unreachable!(),
250        }
251    }
252
253    energized.bytes.iter().filter(|&&b| b).count()
254}