aoc/year2025/day04.rs
1//! # Printing Department
2//!
3//! To speed things up, we build a queue of rolls to be removed. For part one, the length of the
4//! initial list is the answer. For part two, as we process rolls from the list one at a time, we
5//! update neighboring rolls. If any neighbor drops below the threshold, then we add it to the
6//! list. This approach avoids a time-consuming scan of the entire grid to find new rolls to remove.
7//!
8//! Building the initial list of counts, then processing the list is written to minimize branches.
9//! This allows the compiler to auto-vectorize the loops, resulting in a 3.5x speedup.
10use crate::util::grid::*;
11
12type Input = (usize, usize);
13
14pub fn parse(input: &str) -> Input {
15 // Newlines form an empty edge around the grid to avoid boundary checks.
16 let Grid { width, bytes, .. } = Grid::parse_with_border(input);
17 let width = width as usize;
18
19 // Diagonal neighbors relative to the center offset.
20 let center = width + 1;
21 let offsets = [0, 1, 2, width, width + 2, 2 * width, 2 * width + 1, 2 * width + 2];
22
23 // Count neighbors one offset at a time over the whole grid, so that each pass auto-vectorizes.
24 let mut counts = vec![0; bytes.len()];
25
26 for offset in offsets {
27 for (count, &b) in counts[center..].iter_mut().zip(&bytes[offset..]) {
28 *count += u8::from(b == b'@');
29 }
30 }
31
32 // Set empty cells to a value high enough that they will never drop below the threshold.
33 for (count, &b) in counts.iter_mut().zip(&bytes) {
34 *count = if b == b'@' { *count } else { u8::MAX };
35 }
36
37 // Add rolls that can be removed to the initial list. The list has room for every cell, so
38 // pushes always write the next slot then only advance the tail when needed. This avoids
39 // branches.
40 let mut todo = vec![0; bytes.len()];
41 let mut head = 0;
42 let mut tail = 0;
43
44 for (index, &count) in counts.iter().enumerate() {
45 todo[tail] = index;
46 tail += usize::from(count < 4);
47 }
48
49 let part_one = tail;
50
51 // Update neighbors as rolls are removed. If they drop below the threshold, then add to the
52 // list. Each cell is processed at most once.
53 while head < tail {
54 let base = todo[head] - center;
55 head += 1;
56
57 for next in offsets.map(|offset| base + offset) {
58 let count = counts[next];
59 todo[tail] = next;
60 tail += usize::from(count == 4);
61 counts[next] -= 1;
62 }
63 }
64
65 (part_one, tail)
66}
67
68pub fn part1(input: &Input) -> usize {
69 input.0
70}
71
72pub fn part2(input: &Input) -> usize {
73 input.1
74}