kennysliding

December 4, 2025 Transmission_ID: aoc-2025

Advent of Code 2025: Day 4

rust advent of code ai

Problem

Read it here

tl;dr — You have a grid with paper rolls (@) and empty spaces (.). A forklift can access a roll only if it has fewer than 4 neighboring rolls (out of the 8 adjacent positions).

Part 1: Count how many rolls are accessible by forklifts (single pass).

Part 2: Remove accessible rolls iteratively. Once a roll is removed, other rolls may become accessible. Keep removing until no more are accessible. Count the total removed.

Example:

..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.
RoundRemovedRunning Total
11313
21225
3732
4537
5239
6140
7141
8142
9143

Part 1 answer: 13 Part 2 answer: 43

Key insight: For each @, count its 8 neighbors that are also @. If count < 4, it’s accessible. Part 2 is just Part 1 in a loop until nothing changes.


My Solution (Handcrafted)

Data Structure Choice

When I looked at this problem, I thought about what data structure would work best. The options were:

  1. A 2D matrix (simple grid)
  2. An adjacency list (graph-based)

Since the problem is fundamentally about counting neighbors and updating those counts when cells are removed, I went with an adjacency list. This way, each cell knows exactly who its @ neighbors are.

I created a Cell struct to track position and type:

#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)]
struct Cell {
    x: usize,
    y: usize,
    cell_type: char,
}

Part 1: Build and Count

Build the adjacency list by streaming through the grid. For each @ cell, add it as a neighbor to all 8 surrounding cells:

pub fn solve_1(filename: &str) -> usize {
    let input = Self::read_input(filename);
    let grid = input
        .into_iter()
        .map(|line| line.chars().collect::<Vec<char>>())
        .collect::<Vec<Vec<char>>>();

    let adjacency_list: HashMap<Cell, Vec<Cell>> = Self::build_adjacency_list(&grid);

    // find out all the cells that have less than 4 neighbors
    let mut accessible_cells_count = 0;
    for (cell, neighbors) in adjacency_list.iter() {
        if cell.cell_type == '@' && neighbors.len() < 4 {
            accessible_cells_count += 1;
        }
    }

    accessible_cells_count
}

Part 2: Queue-Based Removal

Here’s where the adjacency list pays off. Instead of re-scanning the entire grid each round, I use a queue:

  1. Find all initially accessible cells, add them to the queue
  2. Pop a cell from the queue, remove it
  3. Update all 8 neighbors — remove this cell from their neighbor lists
  4. If any neighbor now has < 4 neighbors, add it to the queue
  5. Repeat until queue is empty
let mut accessible_cells = Vec::new();
for (cell, neighbors) in adjacency_list.iter() {
    if cell.cell_type == '@' && neighbors.len() < 4 {
        accessible_cells.push(cell.clone());
    }
}

let mut total_removed_cells = 0;
loop {
    let current_accessible_cells = accessible_cells.pop();
    if current_accessible_cells.is_none() {
        break;
    }
    let current_accessible_cell = current_accessible_cells.unwrap();

    // ... remove from adjacency list and update neighbors ...

    if let Some(neighbors) = adjacency_list.get_mut(&neighbor_cell) {
        neighbors.retain(|cell| cell != &current_accessible_cell);

        if neighbors.len() < 4 {
            accessible_cells.push(neighbor_cell);
        }
    }
}

This approach only processes cells that actually need updating, rather than scanning the whole grid repeatedly.


Gemini’s Solution

Gemini went with the simpler approach: a 2D grid.

Part 1: Nested Loop

Straightforward — loop through every cell, count neighbors for each @:

fn count_neighbors(grid: &Vec<Vec<char>>, r: i32, c: i32, rows: i32, cols: i32) -> i32 {
    let offsets = [
        (-1, -1), (-1, 0), (-1, 1),
        (0, -1),           (0, 1),
        (1, -1),  (1, 0),  (1, 1),
    ];
    let mut count = 0;
    for &(dr, dc) in &offsets {
        let nr = r + dr;
        let nc = c + dc;
        if nr >= 0 && nr < rows && nc >= 0 && nc < cols {
            if grid[nr as usize][nc as usize] == '@' {
                count += 1;
            }
        }
    }
    count
}

pub fn solve_1(filename: &str) -> i32 {
    let grid = Self::read_input(filename);
    let rows = grid.len() as i32;
    let cols = grid[0].len() as i32;
    let mut accessible_count = 0;

    for r in 0..rows {
        for c in 0..cols {
            if grid[r as usize][c as usize] == '@' {
                if Self::count_neighbors(&grid, r, c, rows, cols) < 4 {
                    accessible_count += 1;
                }
            }
        }
    }

    accessible_count
}

Part 2: Full Re-scan Each Round

Gemini’s Part 2 just runs Part 1 in a loop, removing all accessible cells each round:

pub fn solve_2(filename: &str) -> i32 {
    let mut grid = Self::read_input(filename);
    let rows = grid.len() as i32;
    let cols = grid[0].len() as i32;
    let mut total_removed = 0;

    loop {
        let mut to_remove = Vec::new();

        for r in 0..rows {
            for c in 0..cols {
                if grid[r as usize][c as usize] == '@' {
                    if Self::count_neighbors(&grid, r, c, rows, cols) < 4 {
                        to_remove.push((r, c));
                    }
                }
            }
        }

        if to_remove.is_empty() {
            break;
        }

        total_removed += to_remove.len() as i32;

        for (r, c) in to_remove {
            grid[r as usize][c as usize] = '.';
        }
    }

    total_removed
}

Simple, but it re-scans the entire grid every single round — even cells that couldn’t possibly have changed.


Comparison

AspectMy SolutionGemini’s Solution
Data structAdjacency list (HashMap)2D grid (Vec<Vec<char>>)
Part 1Build graph, then countNested loop with neighbor func
Part 2Queue-based incremental updatesFull grid re-scan each round
ComplexityO(cells removed × 8) updatesO(rounds × grid size)
Code lengthMore verbose, more setupCompact and readable

Reflection

Choosing the right data structure upfront makes a difference.

Part 1: Both approaches work. Gemini’s nested loop is straightforward, my adjacency list requires more setup but models the problem as a graph — which is what it actually is.

Part 2: The adjacency list approach shines here. When you remove a cell, you only update its 8 neighbors instead of re-scanning the entire grid. Gemini’s full re-scan works but does redundant work every round.

Different mental models lead to different solutions. I thought about this as a graph problem from the start, Gemini treated it as a grid simulation. Both are valid, mine just happens to scale better for Part 2.

Time spent: ~30 minutes