Skip to content

Commit c21d7a3

Browse files
aamiguetmerlinorgSethTisue
authored
Add article for day 7 (#901)
Co-authored-by: Merlin Hughes <merlin@merlin.org> Co-authored-by: Seth Tisue <seth@tisue.net>
1 parent e68ff59 commit c21d7a3

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed

docs/2025/puzzles/day07.md

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

33
# Day 7: Laboratories
44

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

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

11+
## Solution Summary
12+
13+
- Parse the input representing the faulty tachyon manifold into an `Array` of `String`.
14+
- In part 1, we count the number of times a tachyon beam is split.
15+
- In part 2, we count all the possible timelines (paths) a tachyon can take in the manifold.
16+
17+
## Parsing the input
18+
19+
Parsing the input is quite straighforward. First let's define a type alias so that we have a meaningful type name for our value:
20+
21+
```scala
22+
type Manifold = Array[String]
23+
```
24+
25+
As the input is a multiline `String` with each line representing a row of the manifold, we simply split it by lines:
26+
27+
```scala
28+
private def parse(input: String): Manifold =
29+
input.split("\n")
30+
```
31+
32+
## Part 1
33+
34+
We have to count the number of times a beam is split. A split occurs when a beam hits a splitter `^` at position `i` . The beam is then split and continue at position `i - 1` and `i + 1` in the next row (line) of the manifold.
35+
36+
We process the manifold in the direction of the beam, top to bottom, row by row. For each row, we have to do two things:
37+
38+
- Count the number of splitters hit by a beam
39+
- Update the positions of the beam for the next row
40+
41+
Let's first parse our manifold and find the initial position of the beam:
42+
43+
```scala
44+
val manifold = parse(input)
45+
val beamSource = Set(manifold.head.indexOf('S'))
46+
```
47+
48+
We then iterate over all the remaining rows using `foldLeft`. Our initial value is composed of the `Set` containing the index of the source of the beam and an initial split count of 0.
49+
50+
At each step we update both the positions of the beam and the cumulative split count and finally return the final count.
51+
52+
```scala
53+
manifold
54+
.tail
55+
.foldLeft((beamSource, 0)):
56+
case ((beamIndices, splitCount), row) =>
57+
val splitIndices = findSplitIndices(row, beamIndices)
58+
val updatedBeamIndices =
59+
beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices
60+
(updatedBeamIndices, splitCount + splitIndices.size)
61+
._2
62+
```
63+
64+
The heavy lifting is done by:
65+
66+
```scala
67+
val splitIndices = findSplitIndices(row, beamIndices)
68+
val updatedBeamIndices =
69+
beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices
70+
(updatedBeamIndices, splitCount + splitIndices.size)
71+
```
72+
73+
First we find all the indices where a hit occurs between a beam and a splitter. This is done in the function `findSplitIndices`.
74+
75+
This function takes two arguments:
76+
77+
- `row` : the current row of the manifold
78+
- `beamIndices` : the resulting `Set` of beam indices from the previous row
79+
80+
We use the fact that a `String` acts like an `Array[Char]`. We zip it with its index and filter it with two conditions :
81+
82+
- A beam is travelling at this index
83+
- There is a splitter at this index
84+
85+
The function returns the list of indices as we don't need anything else.
86+
87+
```scala
88+
private def findSplitIndices(row: String, beamIndices: Set[Int]): List[Int] =
89+
row
90+
.zipWithIndex
91+
.filter: (location, i) =>
92+
beamIndices(i) && location == '^'
93+
.map(_._2)
94+
.toList
95+
```
96+
97+
We now have everything we need for the next step :
98+
99+
From the previous beam indices we compute the new beam indices `updatedBeamIndices`:
100+
101+
- Add the split beam indices : to the right and to the left of each split index.
102+
- Remove the `splitIndices` as the beam is discontinued after a splitter.
103+
104+
```scala
105+
val updatedBeamIndices =
106+
beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices
107+
```
108+
109+
And update the cumulative split count, as `splitIndices` contains only the indices where a splitter is hit, it's simply:
110+
111+
```scala
112+
splitCount + splitIndices.size
113+
```
114+
115+
## Part 2
116+
117+
In part 2, we are tasked to count all the possible timelines (paths) a single tachyon can take in the manifold.
118+
119+
The problem in itself is not much different than part 1 but it has some pitfalls.
120+
121+
We could try to exhaustively compute all the possible paths and count them, but that would be time consuming as the manifold is quite big. Everytime a tachyon hits a splitter, the number of possible futures for this tachyon is doubled!
122+
123+
But we can actually count the number without knowing everything path. To do so we use the following property: all the tachyons reaching a given position `i` at a row `n` share the same future timelines. So we don't need to know their past timelines but only the number of tachyons for each position at each step.
124+
125+
Like in part 1, we parse the manifold and find the original position of the tachyon.
126+
127+
```scala
128+
val manifold = parse(input)
129+
val beamTimelineSource = Map(manifold.head.indexOf('S') -> 1L)
130+
```
131+
132+
Once more we use `foldLeft` to iterate over the manifold. Our accumulator is now the `Map` counting the number of timelines for each tachyon position. Its initial value is the count of the single path the tachyon has taken from the source.
133+
134+
Finally we return the sum of all the timelines count.
135+
136+
```scala
137+
manifold
138+
.tail
139+
.foldLeft(beamTimelineSource): (beamTimelines, row) =>
140+
val splitIndices = findSplitIndices(row, beamTimelines.keySet)
141+
val splitTimelines =
142+
splitIndices
143+
.flatMap: i =>
144+
val pastTimelines = beamTimelines(i)
145+
List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines)
146+
.groupMap(_._1)(_._2)
147+
.view
148+
.mapValues(_.sum)
149+
.toMap
150+
val updatedBeamTimelines =
151+
splitTimelines
152+
.foldLeft(beamTimelines): (bm, s) =>
153+
bm.updatedWith(s._1):
154+
case None => Some(s._2)
155+
case Some(n) => Some(n + s._2)
156+
.removedAll(splitIndices)
157+
updatedBeamTimelines
158+
.values
159+
.sum
160+
```
161+
162+
Let's dive into it!
163+
164+
First, we reuse `findSplitIndices` from part 1 to find the splits.
165+
166+
Then we compute the new timelines originating from each split. Every time a tachyon hits a splitter two new timelines are created: one to the left and one to the right of the splitter. This doubles the number of timelines. Example:
167+
168+
>If a tachyon with 3 different past timelines hits a splitter at position `i`, in the next step we have two possible tachyons with each 3 different past timelines at position `i - 1` and `i + 1` making a total of 6 timelines.
169+
170+
Since we don't care about the past timelines but only the current positions: if multiple splits lead to the same tachyon position, we can group them and sum count of the past timelines which is done by applying `groupMap` and `mapValues` to the resulting `Map`.
171+
172+
Overall this is implemented with:
173+
174+
```scala
175+
val splitTimelines =
176+
splitIndices
177+
.flatMap: i =>
178+
// splitting a timeline
179+
val pastTimelines = beamTimelines(i)
180+
List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines)
181+
// grouping and summing timelines by resulting position
182+
.groupMap(_._1)(_._2)
183+
.view
184+
.mapValues(_.sum)
185+
.toMap
186+
```
187+
188+
From the previous beam timelines map we finally compute the new beam timelines `updatedBeamTimelines`:
189+
190+
- Merging the split timelines `Map`. By using `updateWith` we handle the two cases:
191+
- If the entry already exists, we udpate it by adding the new timeline count to the existing one
192+
- Or creating a new entry
193+
- Removing all positions that hit a splitter
194+
195+
```scala
196+
val updatedBeamTimelines =
197+
splitTimelines
198+
.foldLeft(beamTimelines): (bm, s) =>
199+
bm.updatedWith(s._1):
200+
// adding a new key
201+
case None => Some(s._2)
202+
// updating a value by summing both timeline counts
203+
case Some(n) => Some(n + s._2)
204+
.removedAll(splitIndices)
205+
```
206+
207+
## Final code
208+
209+
```scala
210+
type Manifold = Array[String]
211+
212+
private def parse(input: String): Manifold =
213+
input.split("\n")
214+
215+
private def findSplitIndices(row: String, beamIndices: Set[Int]): List[Int] =
216+
row
217+
.zipWithIndex
218+
.filter: (location, i) =>
219+
beamIndices(i) && location == '^'
220+
.map(_._2)
221+
.toList
222+
223+
override def part1(input: String): Long =
224+
val manifold = parse(input)
225+
val beamSource = Set(manifold.head.indexOf('S'))
226+
manifold
227+
.tail
228+
.foldLeft((beamSource, 0)):
229+
case ((beamIndices, splitCount), row) =>
230+
val splitIndices = findSplitIndices(row, beamIndices)
231+
val updatedBeamIndices =
232+
beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices
233+
(updatedBeamIndices, splitCount + splitIndices.size)
234+
._2
235+
236+
override def part2(input: String): Long =
237+
val manifold = parse(input)
238+
val beamTimelineSource = Map(manifold.head.indexOf('S') -> 1L)
239+
manifold
240+
.tail
241+
.foldLeft(beamTimelineSource): (beamTimelines, row) =>
242+
val splitIndices = findSplitIndices(row, beamTimelines.keySet)
243+
val splitTimelines =
244+
splitIndices
245+
.flatMap: i =>
246+
val pastTimelines = beamTimelines(i)
247+
List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines)
248+
.groupMap(_._1)(_._2)
249+
.view
250+
.mapValues(_.sum)
251+
.toMap
252+
val updatedBeamTimelines =
253+
splitTimelines
254+
.foldLeft(beamTimelines): (bm, s) =>
255+
bm.updatedWith(s._1):
256+
case None => Some(s._2)
257+
case Some(n) => Some(n + s._2)
258+
.removedAll(splitIndices)
259+
updatedBeamTimelines
260+
.values
261+
.sum
262+
```
263+
9264
## Solutions from the community
10265

11266
- [Solution](https://github.com/rmarbeck/advent2025/blob/main/day07/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck)
@@ -15,6 +270,7 @@ https://adventofcode.com/2025/day/7
15270
- [Solution](https://github.com/guycastle/advent_of_code/blob/main/src/main/scala/aoc2025/day07/DaySeven.scala) by [Guillaume Vandecasteele](https://github.com/guycastle)
16271
- [Solution](https://github.com/YannMoisan/advent-of-code/blob/master/2025/src/main/scala/Day7.scala) by [Yann Moisan](https://github.com/YannMoisan)
17272
- [Solution](https://github.com/Jannyboy11/AdventOfCode2025/blob/master/src/main/scala/day07/Day07.scala) by [Jan Boerman](https://x.com/JanBoerman95)
273+
- [Solution](https://github.com/aamiguet/advent-2025/blob/main/src/main/scala/ch/aamiguet/advent2025/Day07.scala) by [Antoine Amiguet](https://github.com/aamiguet)
18274

19275
Share your solution to the Scala community by editing this page.
20276
You can even write the whole article! [Go here to volunteer](https://github.com/scalacenter/scala-advent-of-code/discussions/842)

0 commit comments

Comments
 (0)