Skip to content

Commit 284408a

Browse files
authored
Add article for day 04 (#881)
1 parent 4a89b79 commit 284408a

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

docs/2025/puzzles/day04.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,240 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 4: Printing Department
44

5+
by [@philippus](https://github.com/philippus)
6+
57
## Puzzle description
68

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

11+
## Solution Summary
12+
13+
- parse the input into a two-dimensional array representing the grid.
14+
- for part 1, count the accessible rolls of paper by checking all the adjacent positions for all the rolls in the grid.
15+
- for part 2, use the method created in part 1 repeatedly while updating the grid and keeping count until there are no
16+
more accessible rolls.
17+
18+
### Part 1
19+
20+
First the input string needs to be parsed. A two-dimensional array of characters (`Array[Array[Char]]`) is used to
21+
represent the grid. This makes it easy to reason about positions in the grid. And it also helps with speed, because we
22+
can update the array. Split the input by the newline character, giving the rows in the grid. Than map each row, calling
23+
`toCharArray`.
24+
25+
```scala
26+
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
27+
```
28+
29+
It's nice to be able to visualize the grid, just like in the puzzle description. This can be done like this:
30+
31+
```scala
32+
def drawGrid(grid: Array[Array[Char]]): String =
33+
grid.map(_.mkString).mkString("\n") :+ '\n'
34+
```
35+
36+
Calling `println(drawGrid)` on the sample input would give the following:
37+
38+
```
39+
..@@.@@@@.
40+
@@@.@.@.@@
41+
@@@@@.@.@@
42+
@.@@@@..@.
43+
@@.@@@@.@@
44+
.@@@@@@@.@
45+
.@.@.@.@@@
46+
@.@@@.@@@@
47+
.@@@@@@@@.
48+
@.@.@@@.@.
49+
```
50+
51+
This can also be helpful to figure out subtle bugs in the solution.
52+
53+
A helper method `countAdjacentRolls` is created that counts the rolls of paper (@) in the 8 (or less, because of the
54+
edges of the grid) adjacent positions in the grid for a given position.
55+
56+
```scala
57+
def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
58+
val adjacentRolls =
59+
for
60+
cy <- pos.y - 1 to pos.y + 1
61+
cx <- pos.x - 1 to pos.x + 1
62+
if (cx, cy) != (pos.x, pos.y) // exclude given position
63+
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
64+
candidate = grid(cy)(cx)
65+
if candidate == '@'
66+
yield
67+
candidate
68+
adjacentRolls.length
69+
```
70+
71+
To count all the accessible rolls of paper, all the positions in the grid containing a roll (@) should be checked for
72+
the amount of adjacent rolls. Using calls to the `indices` method of the array the positions are generated. If a
73+
position contains a roll and the amount of adjacent rolls for that position is less than 4 it gets counted towards the
74+
total sum. The `countAccessibleRoll` method looks like this:
75+
76+
```scala
77+
def countAccessibleRolls(grid: Array[Array[Char]]): Int =
78+
(for
79+
y <- grid.indices
80+
x <- grid(y).indices
81+
yield if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4 then 1 else 0).sum
82+
```
83+
84+
This already gives the correct result, but it can be made a bit nicer by also updating the grid with `x`s and showing
85+
the result:
86+
87+
```scala
88+
def countAccessibleRollsAndUpdateGrid(grid: Array[Array[Char]]): Int =
89+
var count = 0
90+
for
91+
y <- grid.indices
92+
x <- grid(y).indices
93+
if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4
94+
do
95+
count += 1
96+
grid(y)(x) = 'x'
97+
count
98+
```
99+
100+
Since the grid is now updated during the loop with `x`s, the `countAdjacentRolls` needs an extra (`|| candidate == 'x'`) condition, the
101+
updated method looks like this:
102+
103+
```scala
104+
def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
105+
val adjacentRolls =
106+
for
107+
cy <- pos.y - 1 to pos.y + 1
108+
cx <- pos.x - 1 to pos.x + 1
109+
if (cx, cy) != (pos.x, pos.y) // exclude given position
110+
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
111+
candidate = grid(cy)(cx)
112+
if candidate == '@' || candidate == 'x'
113+
yield
114+
candidate
115+
adjacentRolls.length
116+
```
117+
118+
Calling `println(drawGrid)` after calling `countAccessibleRollsAndUpdateGrid(grid)` gives:
119+
120+
```
121+
..xx.xx@x.
122+
x@@.@.@.@@
123+
@@@@@.x.@@
124+
@.@@@@..@.
125+
x@.@@@@.@x
126+
.@@@@@@@.@
127+
.@.@.@.@@@
128+
x.@@@.@@@@
129+
.@@@@@@@@.
130+
x.x.@@@.x.
131+
```
132+
133+
neat!
134+
135+
### Part 2
136+
137+
To count all the removable rolls of paper the `countAccessibleRollsAndUpdateGrid` method can be used repeatedly in a
138+
while loop, making sure that after each iteration, all the `x`s in the grid are replaced with a `.`. The complete
139+
`countRemovableRolls` method looks like this:
140+
141+
```scala
142+
def countRemovableRolls(grid: Array[Array[Char]]): Int =
143+
var count = 0
144+
var done = false
145+
while !done do
146+
val accessible = countAccessibleRollsAndUpdateGrid(grid)
147+
if accessible == 0 then
148+
done = true
149+
else
150+
count += accessible
151+
for
152+
y <- grid.indices
153+
x <- grid(y).indices
154+
if grid(y)(x) == 'x'
155+
do
156+
grid(y)(x) = '.'
157+
count
158+
```
159+
160+
Calling `println(drawGrid)` after calling `countRemovableRolls(grid)` gives:
161+
162+
```
163+
..........
164+
..........
165+
..........
166+
....@@....
167+
...@@@@...
168+
...@@@@@..
169+
...@.@.@@.
170+
...@@.@@@.
171+
...@@@@@..
172+
....@@@...
173+
```
174+
175+
again exactly the same as in the puzzle description!
176+
177+
## Final Code
178+
179+
```scala
180+
def part1(input: String): Long =
181+
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
182+
countAccessibleRolls(grid)
183+
184+
def part2(input: String): Long =
185+
val grid: Array[Array[Char]] = input.split('\n').map(_.toCharArray)
186+
countRemovableRolls(grid)
187+
188+
def countAdjacentRolls(grid: Array[Array[Char]], pos: (x: Int, y: Int)): Int =
189+
val adjacentRolls =
190+
for
191+
cy <- pos.y - 1 to pos.y + 1
192+
cx <- pos.x - 1 to pos.x + 1
193+
if (cx, cy) != (pos.x, pos.y) // exclude given position
194+
if cy >= 0 && cy < grid.length && cx >= 0 && cx < grid(cy).length // exclude out of bounds positions
195+
candidate = grid(cy)(cx)
196+
if candidate == '@' || candidate == 'x'
197+
yield
198+
candidate
199+
adjacentRolls.length
200+
201+
def countAccessibleRolls(grid: Array[Array[Char]]): Int =
202+
(for
203+
y <- grid.indices
204+
x <- grid(y).indices
205+
yield if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4 then 1 else 0).sum
206+
207+
def countAccessibleRollsAndUpdateGrid(grid: Array[Array[Char]]): Int =
208+
var count = 0
209+
for
210+
y <- grid.indices
211+
x <- grid(y).indices
212+
if grid(y)(x) == '@' && countAdjacentRolls(grid, (x, y)) < 4
213+
do
214+
count += 1
215+
grid(y)(x) = 'x'
216+
count
217+
218+
def countRemovableRolls(grid: Array[Array[Char]]): Int =
219+
var count = 0
220+
var done = false
221+
while !done do
222+
val accessible = countAccessibleRollsAndUpdateGrid(grid)
223+
if accessible == 0 then
224+
done = true
225+
else
226+
count += accessible
227+
for
228+
y <- grid.indices
229+
x <- grid(y).indices
230+
if grid(y)(x) == 'x'
231+
do
232+
grid(y)(x) = '.'
233+
count
234+
235+
def drawGrid(grid: Array[Array[Char]]): String =
236+
grid.map(_.mkString).mkString("\n") :+ '\n'
237+
```
238+
9239
## Solutions from the community
10240

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

0 commit comments

Comments
 (0)