Skip to main content

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}