Metadata
-
Date
-
Tagged
-
Part of series
- Advent of Code 2025 Day 1
- Advent of Code 2025 Day 2
- Advent of Code 2025 Day 3
- Advent of Code 2025 Day 4
- Advent of Code 2025 Day 5
- Advent of Code 2025 Day 6
- Advent of Code 2025 Day 7
- Advent of Code 2025 Day 8
- Advent of Code 2025 Day 9
- Advent of Code 2025 Day 10
- Advent of Code 2025 Day 11
- Advent of Code 2025 Day 12
-
Older post
-
Newer post
Advent of Code 2025 Day 4
Day 4: Printing Department
https://adventofcode.com/2025/day/4
You arrive at a warehouse filled with rolls of paper. That’s the input for today, a 2D-map of the warehouse.
An example input looks like this:
..@@.@@@@.@@@.@.@.@@@@@@@.@.@@@.@@@@..@.@@.@@@@.@@.@@@@@@@.@.@.@.@.@@@@.@@@.@@@@.@@@@@@@@.@.@.@@@.@.@is paper.is empty
Paper can only be accessed if there are fewer than 4 rolls of paper near it.
”Near” means the 8 neighbouring positions.
Helpers
It’s time! It’s a 2D grid puzzle, time to make a Point data type.
You don’t need to do this if you’re reading this, but I like to.
While I’m at it, I created a helper to find the 8 neighbours a Point has,
and even a function that counts how many rolls of paper are in those positions.
If you’re confused about the weird logic in that neighbour function (the checked_add_signed(dr)?),
what it’s really doing is point + delta.
But Rust has signed and unsigned integers and forces you to deal with under/overflows.
That ? handles that uncertainty, I call it the Annie operator.
#[derive(Debug, Clone, Copy, Hash, Eq, Ord, PartialEq, PartialOrd)]struct Point { row: usize, col: usize,}
const DELTAS: [(isize, isize); 8] = [ (-1, 0), // up (-1, 1), // up right (0, 1), // right (1, 1), // down right (1, 0), // down (1, -1), // down left (0, -1), // left (-1, -1), // up left];
impl Point { fn neighbours(&self) -> Vec<Point> { DELTAS .iter() .filter_map(|&(dr, dc)| { Some(Point { row: self.row.checked_add_signed(dr)?, col: self.col.checked_add_signed(dc)?, }) }) .collect() }
fn count_neighbours(&self, map: &[Vec<char>]) -> usize { self.neighbours() .iter() .filter(|point| { matches!( map.get(point.row).and_then(|row| row.get(point.col)), Some('@') ) }) .count() }}Part 1
The question asks how many rolls of paper can be accessed.
fn part_1(input: &str) -> usize { let map: Vec<Vec<char>> = input.lines().map(|l| l.chars().collect()).collect();
let mut sum = 0; for (row, line) in map.iter().enumerate() { for (col, c) in line.iter().enumerate() { if *c != '@' { continue; } let point = Point { row, col }; if point.count_neighbours(&map) < 4 { sum += 1; } } } sum}Part 2
Once you complete a first wave of paper removal, the elves want you to do it again, and again, and again, …
Until no paper is removed.
The logic is split up in 2 parts:
- Marking rolls to be removed
- Removing the marked rolls
I do it this way because removing a roll during the first loop might cause a next roll to be removed early. It turns out that this leads to the same result because you keep going until no roll is able to be removed.
fn part_2(input: &str) -> usize { let mut map: Vec<Vec<char>> = input.lines().map(|line| line.chars().collect()).collect();
let mut sum = 0; loop { let mut to_remove = Vec::new(); for (row, line) in map.iter().enumerate() { for (col, c) in line.iter().enumerate() { if *c != '@' { continue; } let point = Point { row, col }; if point.count_neighbours(&map) < 4 { to_remove.push(point); } } }
if to_remove.is_empty() { break; } sum += to_remove.len(); for point in to_remove { map[point.row][point.col] = '.'; } } sum}Final code
#[derive(Debug, Clone, Copy, Hash, Eq, Ord, PartialEq, PartialOrd)]struct Point { row: usize, col: usize,}
const DELTAS: [(isize, isize); 8] = [ (-1, 0), // up (-1, 1), // up right (0, 1), // right (1, 1), // down right (1, 0), // down (1, -1), // down left (0, -1), // left (-1, -1), // up left];
impl Point { fn neighbours(&self) -> Vec<Point> { DELTAS .iter() .filter_map(|&(dr, dc)| { Some(Point { row: self.row.checked_add_signed(dr)?, col: self.col.checked_add_signed(dc)?, }) }) .collect() }
fn count_neighbours(&self, map: &[Vec<char>]) -> usize { self.neighbours() .iter() .filter(|point| { matches!( map.get(point.row).and_then(|row| row.get(point.col)), Some('@') ) }) .count() }}
pub fn part_1(input: &str) -> usize { let map: Vec<Vec<char>> = input.lines().map(|l| l.chars().collect()).collect();
let mut sum = 0; for (row, line) in map.iter().enumerate() { for (col, c) in line.iter().enumerate() { if *c != '@' { continue; } let point = Point { row, col }; if point.count_neighbours(&map) < 4 { sum += 1; } } } sum}
fn part_2(input: &str) -> usize { let mut map: Vec<Vec<char>> = input.lines().map(|line| line.chars().collect()).collect();
let mut sum = 0; loop { let mut to_remove = Vec::new(); for (row, line) in map.iter().enumerate() { for (col, c) in line.iter().enumerate() { if *c != '@' { continue; } let point = Point { row, col }; if point.count_neighbours(&map) < 4 { to_remove.push(point); } } }
if to_remove.is_empty() { break; } sum += to_remove.len(); for point in to_remove { map[point.row][point.col] = '.'; } } sum}