Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions docs/2025/puzzles/day04.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,240 @@ import Solver from "../../../../../website/src/components/Solver.js"

# Day 4: Printing Department

by [@philippus](https://github.com/philippus)

## Puzzle description

https://adventofcode.com/2025/day/4

## Solution Summary

- parse the input into a two-dimensional array representing the grid.
- for part 1, count the accessible rolls of paper by checking all the adjacent positions for all the rolls in the grid.
- for part 2, use the method created in part 1 repeatedly while updating the grid and keeping count until there are no
more accessible rolls.

### Part 1

First the input string needs to be parsed. A two-dimensional array of characters (`Array[Array[Char]]`) is used to
represent the grid. This makes it easy to reason about positions in the grid. And it also helps with speed, because we
can update the array. Split the input by the newline character, giving the rows in the grid. Than map each row, calling
`toCharArray`.

```scala
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
```

It's nice to be able to visualize the grid, just like in the puzzle description. This can be done like this:

```scala
def drawGrid(grid: Array[Array[Char]]): String =
grid.map(_.mkString).mkString("\n") :+ '\n'
```

Calling `println(drawGrid)` on the sample input would give the following:

```
..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.
```

This can also be helpful to figure out subtle bugs in the solution.

A helper method `countAdjacentRolls` is created that counts the rolls of paper (@) in the 8 (or less, because of the
edges of the grid) adjacent positions in the grid for a given position.

```scala
def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
val adjacentRolls =
for
cy <- pos.y - 1 to pos.y + 1
cx <- pos.x - 1 to pos.x + 1
if (cx, cy) != (pos.x, pos.y) // exclude given position
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
candidate = grid(cy)(cx)
if candidate == '@'
yield
candidate
adjacentRolls.length
```

To count all the accessible rolls of paper, all the positions in the grid containing a roll (@) should be checked for
the amount of adjacent rolls. Using calls to the `indices` method of the array the positions are generated. If a
position contains a roll and the amount of adjacent rolls for that position is less than 4 it gets counted towards the
total sum. The `countAccessibleRoll` method looks like this:

```scala
def countAccessibleRolls(grid: Array[Array[Char]]): Int =
(for
y <- grid.indices
x <- grid(y).indices
yield if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4 then 1 else 0).sum
```

This already gives the correct result, but it can be made a bit nicer by also updating the grid with `x`s and showing
the result:

```scala
def countAccessibleRollsAndUpdateGrid(grid: Array[Array[Char]]): Int =
var count = 0
for
y <- grid.indices
x <- grid(y).indices
if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4
do
count += 1
grid(y)(x) = 'x'
count
```

Since the grid is now updated during the loop with `x`s, the `countAdjacentRolls` needs an extra (`|| candidate == 'x'`) condition, the
updated method looks like this:

```scala
def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
val adjacentRolls =
for
cy <- pos.y - 1 to pos.y + 1
cx <- pos.x - 1 to pos.x + 1
if (cx, cy) != (pos.x, pos.y) // exclude given position
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
candidate = grid(cy)(cx)
if candidate == '@' || candidate == 'x'
yield
candidate
adjacentRolls.length
```

Calling `println(drawGrid)` after calling `countAccessibleRollsAndUpdateGrid(grid)` gives:

```
..xx.xx@x.
x@@.@.@.@@
@@@@@.x.@@
@.@@@@..@.
x@.@@@@.@x
.@@@@@@@.@
.@.@.@.@@@
x.@@@.@@@@
.@@@@@@@@.
x.x.@@@.x.
```

neat!

### Part 2

To count all the removable rolls of paper the `countAccessibleRollsAndUpdateGrid` method can be used repeatedly in a
while loop, making sure that after each iteration, all the `x`s in the grid are replaced with a `.`. The complete
`countRemovableRolls` method looks like this:

```scala
def countRemovableRolls(grid: Array[Array[Char]]): Int =
var count = 0
var done = false
while !done do
val accessible = countAccessibleRollsAndUpdateGrid(grid)
if accessible == 0 then
done = true
else
count += accessible
for
y <- grid.indices
x <- grid(y).indices
if grid(y)(x) == 'x'
do
grid(y)(x) = '.'
count
```

Calling `println(drawGrid)` after calling `countRemovableRolls(grid)` gives:

```
..........
..........
..........
....@@....
...@@@@...
...@@@@@..
...@.@.@@.
...@@.@@@.
...@@@@@..
....@@@...
```

again exactly the same as in the puzzle description!

## Final Code

```scala
def part1(input: String): Long =
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
countAccessibleRolls(grid)

def part2(input: String): Long =
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
countRemovableRolls(grid)

def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
val adjacentRolls =
for
cy <- pos.y - 1 to pos.y + 1
cx <- pos.x - 1 to pos.x + 1
if (cx, cy) != (pos.x, pos.y) // exclude given position
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
candidate = grid(cy)(cx)
if candidate == '@' || candidate == 'x'
yield
candidate
adjacentRolls.length

def countAccessibleRolls(grid: Array[Array[Char]]): Int =
(for
y <- grid.indices
x <- grid(y).indices
yield if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4 then 1 else 0).sum

def countAccessibleRollsAndUpdateGrid(grid: Array[Array[Char]]): Int =
var count = 0
for
y <- grid.indices
x <- grid(y).indices
if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4
do
count += 1
grid(y)(x) = 'x'
count

def countRemovableRolls(grid: Array[Array[Char]]): Int =
var count = 0
var done = false
while !done do
val accessible = countAccessibleRollsAndUpdateGrid(grid)
if accessible == 0 then
done = true
else
count += accessible
for
y <- grid.indices
x <- grid(y).indices
if grid(y)(x) == 'x'
do
grid(y)(x) = '.'
count

def drawGrid(grid: Array[Array[Char]]): String =
grid.map(_.mkString).mkString("\n") :+ '\n'
```

## Solutions from the community

Share your solution to the Scala community by editing this page.
Expand Down