From 76fa98e7a7a9d5b88b33096ebfd18c995c7f2249 Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 15:14:01 +0100 Subject: [PATCH 1/7] Add article for day 7 --- docs/2025/puzzles/day07.md | 258 ++++++++++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 2 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index 4415390ea..87755a2c9 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -2,14 +2,268 @@ import Solver from "../../../../../website/src/components/Solver.js" # Day 7: Laboratories +by [@aamiguet](https://github.com/aamiguet/) + ## Puzzle description https://adventofcode.com/2025/day/7 +## Solution Summary + +- Parse the input representing the faulty tachyon manifold into a two dimensional `Array`. +- In part 1, we count the number of times a tachyon beam is splitted. +- In part 2, we count all the possible timelines (paths) a tachyon can take in the manifold. + +## Parsing the input + +Parsing the input is quite straighforward. First let's define a type alias so that we have a meaningful type name for our value: + +```scala +type Manifold = Array[Array[Char]] +``` + +As the input is a NxM grid of characters, we split it by line and then for each line convert it from a `String` to an `Array[Char]` + +```scala +private def parse(input: String): Manifold = + input.split("\n").map(_.toArray) +``` + +## Part 1 + +We have to count the number of times a beam is splitted. 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. + +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 : + +- Count the number of splitters hit by a beam +- Update the positions of the beam for the next row + +Let's first parse our manifold and find the initial position of the beam: + +```scala +val manifold = parse(input) +val beamSource = Set(manifold.head.indexOf('S')) +``` + +We then iterate over all the remainging rows using `foldLeft`. Our initial value is composed ot the `Set` containing the index of the source of the beam and an initial split count of 0. + +At each step we update both the positions of the beam and the cumulative split count and finally returns the final count. + +```scala +manifold + .tail + .foldLeft((beamSource, 0)): + case ((beamIndices, splitCount), row) => + val splitIndices = findSplitIndices(row, beamIndices) + val updatedBeamIndices = + beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices + (updatedBeamIndices, splitCount + splitIndices.size) + ._2 +``` + +The heavy lifting is done by: + +```scala +val splitIndices = findSplitIndices(row, beamIndices) +val updatedBeamIndices = + beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices +(updatedBeamIndices, splitCount + splitIndices.size) +``` + +First we find all the indices where a hit occurs between a beam and a splitter. This is done in the function `findSplitIndices`. + +This function takes two arguments: + +- `row` : the current row of the manifold +- `beamIndices` : the resulting `Set` of beam indices from the previous row + +We zip the `row` with its index and then filter it with two conditions : + +- A beam is travelling at this index +- There is a splitter at this index + +The function returns the list of indices as we don't need anything else. + +```scala +private def findSplitIndices(row: Array[Char], beamIndices: Set[Int]): List[Int] = + row + .zipWithIndex + .filter: (location, i) => + beamIndices(i) && location == '^' + .map(_._2) + .toList +``` + +We now have everything we need for the next step : + +From the previous beam indices we compute the new beam indices `updatedBeamIndices`: + +- Add the splitted beam indices : to the right and to the left of each split index. +- Remove the `splitIndices` as the beam is discontinued after a splitter. + +```scala +val updatedBeamIndices = + beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices +``` + +And update the cumulative split count, as `splitIndices` contains only the indices where a splitter is hit, it's simply: + +```scala +splitCount + splitIndices.size +``` + +## Part 2 + +In part 2, we are tasked to count all the possible timelines (paths) a single tachyon can take in the manifold. + +The problem in itself is not much different than part 1 but it has some pitfalls. + +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! + +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. + +Like in part 1, we parse the manifold and find the original position of the tachyon. + +```scala +val manifold = parse(input) +val beamTimelineSource = Map(manifold.head.indexOf('S') -> 1L) +``` + +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. + +Finally we return the sum of all the timelines count. + +```scala +manifold + .tail + .foldLeft(beamTimelineSource): (beamTimelines, row) => + val splitIndices = findSplitIndices(row, beamTimelines.keySet) + val splittedTimelines = + splitIndices + .flatMap: i => + val pastTimelines = beamTimelines(i) + List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines) + .groupMap(_._1)(_._2) + .view + .mapValues(_.sum) + .toMap + val updatedBeamTimelines = + splittedTimelines + .foldLeft(beamTimelines): (bm, s) => + bm.updatedWith(s._1): + case None => Some(s._2) + case Some(n) => Some(n + s._2) + .removedAll(splitIndices) + updatedBeamTimelines + .values + .sum +``` + +Let's dive into it! + +First, we reuse `findSplitIndices` from part 1 to find the splits. + +Then we compute the new timelines originating from each split. Everytime 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: + +>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. + +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`. + +Overall this is implemented with: + +```scala +val splittedTimelines = + splitIndices + .flatMap: i => + // splitting a timeline + val pastTimelines = beamTimelines(i) + List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines) + // grouping and summing timelines by resulting position + .groupMap(_._1)(_._2) + .view + .mapValues(_.sum) + .toMap +``` + +From the previous beam timelines map we finally compute the new beam timelines `updatedBeamTimelines`: + +- Merging the splitted timelines `Map`. By using `updateWith` we handle the two cases: + - If the entry already exists, we udpate it by adding the new timeline count to the existing one + - Or creating a new entry +- Removing all positions that hit a splitter + +```scala +val updatedBeamTimelines = + splittedTimelines + .foldLeft(beamTimelines): (bm, s) => + bm.updatedWith(s._1): + // adding a new key + case None => Some(s._2) + // updating a value by summing both timeline counts + case Some(n) => Some(n + s._2) + .removedAll(splitIndices) +``` + +## Final code + +```scala +type Manifold = Array[Array[Char]] + +private def parse(input: String): Manifold = + input.split("\n").map(_.toArray) + +private def findSplitIndices(row: Array[Char], beamIndices: Set[Int]): List[Int] = + row + .zipWithIndex + .filter: (location, i) => + beamIndices(i) && location == '^' + .map(_._2) + .toList + +override def part1(input: String): Long = + val manifold = parse(input) + val beamSource = Set(manifold.head.indexOf('S')) + manifold + .tail + .foldLeft((beamSource, 0)): + case ((beamIndices, splitCount), row) => + val splitIndices = findSplitIndices(row, beamIndices) + val updatedBeamIndices = + beamIndices ++ splitIndices.flatMap(i => Set(i - 1, i + 1)) -- splitIndices + (updatedBeamIndices, splitCount + splitIndices.size) + ._2 + +override def part2(input: String): Long = + val manifold = parse(input) + val beamTimelineSource = Map(manifold.head.indexOf('S') -> 1L) + manifold + .tail + .foldLeft(beamTimelineSource): (beamTimelines, row) => + val splitIndices = findSplitIndices(row, beamTimelines.keySet) + val splittedTimelines = + splitIndices + .flatMap: i => + val pastTimelines = beamTimelines(i) + List((i + 1) -> pastTimelines, (i - 1) -> pastTimelines) + .groupMap(_._1)(_._2) + .view + .mapValues(_.sum) + .toMap + val updatedBeamTimelines = + splittedTimelines + .foldLeft(beamTimelines): (bm, s) => + bm.updatedWith(s._1): + case None => Some(s._2) + case Some(n) => Some(n + s._2) + .removedAll(splitIndices) + updatedBeamTimelines + .values + .sum +``` + ## Solutions from the community -- [Solution](https://github.com/rmarbeck/advent2025/blob/main/day07/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck) -- [Solution](https://github.com/Philippus/adventofcode/blob/main/src/main/scala/adventofcode2025/Day07.scala) by [Philippus Baalman](https://github.com/philippus) +- [Solution](https://github.com/aamiguet/advent-2025/blob/main/src/main/scala/ch/aamiguet/advent2025/Day07.scala) by [Antoine Amiguet](https://github.com/aamiguet) Share your solution to the Scala community by editing this page. You can even write the whole article! [Go here to volunteer](https://github.com/scalacenter/scala-advent-of-code/discussions/842) From 2aa73bd5c6e8544125a81912648769988909a270 Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 17:08:30 +0100 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: Merlin Hughes --- docs/2025/puzzles/day07.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index 87755a2c9..61d86fad1 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -11,7 +11,7 @@ https://adventofcode.com/2025/day/7 ## Solution Summary - Parse the input representing the faulty tachyon manifold into a two dimensional `Array`. -- In part 1, we count the number of times a tachyon beam is splitted. +- In part 1, we count the number of times a tachyon beam is split. - In part 2, we count all the possible timelines (paths) a tachyon can take in the manifold. ## Parsing the input @@ -31,7 +31,7 @@ private def parse(input: String): Manifold = ## Part 1 -We have to count the number of times a beam is splitted. 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. +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. 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 : @@ -45,9 +45,9 @@ val manifold = parse(input) val beamSource = Set(manifold.head.indexOf('S')) ``` -We then iterate over all the remainging rows using `foldLeft`. Our initial value is composed ot the `Set` containing the index of the source of the beam and an initial split count of 0. +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. -At each step we update both the positions of the beam and the cumulative split count and finally returns the final count. +At each step we update both the positions of the beam and the cumulative split count and finally return the final count. ```scala manifold @@ -98,7 +98,7 @@ We now have everything we need for the next step : From the previous beam indices we compute the new beam indices `updatedBeamIndices`: -- Add the splitted beam indices : to the right and to the left of each split index. +- Add the split beam indices : to the right and to the left of each split index. - Remove the `splitIndices` as the beam is discontinued after a splitter. ```scala @@ -163,7 +163,7 @@ Let's dive into it! First, we reuse `findSplitIndices` from part 1 to find the splits. -Then we compute the new timelines originating from each split. Everytime 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: +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: >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. @@ -187,7 +187,7 @@ val splittedTimelines = From the previous beam timelines map we finally compute the new beam timelines `updatedBeamTimelines`: -- Merging the splitted timelines `Map`. By using `updateWith` we handle the two cases: +- Merging the split timelines `Map`. By using `updateWith` we handle the two cases: - If the entry already exists, we udpate it by adding the new timeline count to the existing one - Or creating a new entry - Removing all positions that hit a splitter From c5838199c47c3531f9a4ceb4388d12a88e3d9524 Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 17:13:51 +0100 Subject: [PATCH 3/7] Restore community solution links --- docs/2025/puzzles/day07.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index 61d86fad1..a257cda9e 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -263,6 +263,10 @@ override def part2(input: String): Long = ## Solutions from the community +- [Solution](https://github.com/rmarbeck/advent2025/blob/main/day07/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck) + +- [Solution](https://github.com/Philippus/adventofcode/blob/main/src/main/scala/adventofcode2025/Day07.scala) by [Philippus Baalman](https://github.com/philippus) + - [Solution](https://github.com/aamiguet/advent-2025/blob/main/src/main/scala/ch/aamiguet/advent2025/Day07.scala) by [Antoine Amiguet](https://github.com/aamiguet) Share your solution to the Scala community by editing this page. From 2098d33e968b987f1bfc78a640ab98d7991d8a81 Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 17:44:29 +0100 Subject: [PATCH 4/7] Fix variable names --- docs/2025/puzzles/day07.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index a257cda9e..c0ed6fe1a 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -138,7 +138,7 @@ manifold .tail .foldLeft(beamTimelineSource): (beamTimelines, row) => val splitIndices = findSplitIndices(row, beamTimelines.keySet) - val splittedTimelines = + val splitTimelines = splitIndices .flatMap: i => val pastTimelines = beamTimelines(i) @@ -148,7 +148,7 @@ manifold .mapValues(_.sum) .toMap val updatedBeamTimelines = - splittedTimelines + splitTimelines .foldLeft(beamTimelines): (bm, s) => bm.updatedWith(s._1): case None => Some(s._2) @@ -240,7 +240,7 @@ override def part2(input: String): Long = .tail .foldLeft(beamTimelineSource): (beamTimelines, row) => val splitIndices = findSplitIndices(row, beamTimelines.keySet) - val splittedTimelines = + val splitTimelines = splitIndices .flatMap: i => val pastTimelines = beamTimelines(i) @@ -250,7 +250,7 @@ override def part2(input: String): Long = .mapValues(_.sum) .toMap val updatedBeamTimelines = - splittedTimelines + splitTimelines .foldLeft(beamTimelines): (bm, s) => bm.updatedWith(s._1): case None => Some(s._2) From 94d548afb2307953042ef594472549da62b04f84 Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 17:56:34 +0100 Subject: [PATCH 5/7] Replace Array[Char] with String --- docs/2025/puzzles/day07.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index c0ed6fe1a..6a91b164f 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -10,7 +10,7 @@ https://adventofcode.com/2025/day/7 ## Solution Summary -- Parse the input representing the faulty tachyon manifold into a two dimensional `Array`. +- Parse the input representing the faulty tachyon manifold into an `Array` of `String`. - In part 1, we count the number of times a tachyon beam is split. - In part 2, we count all the possible timelines (paths) a tachyon can take in the manifold. @@ -19,14 +19,14 @@ https://adventofcode.com/2025/day/7 Parsing the input is quite straighforward. First let's define a type alias so that we have a meaningful type name for our value: ```scala -type Manifold = Array[Array[Char]] +type Manifold = Array[String] ``` -As the input is a NxM grid of characters, we split it by line and then for each line convert it from a `String` to an `Array[Char]` +As the input is a multiline `String` with each line representing a row of the manifold, we simply split by lines: ```scala private def parse(input: String): Manifold = - input.split("\n").map(_.toArray) + input.split("\n") ``` ## Part 1 @@ -77,7 +77,7 @@ This function takes two arguments: - `row` : the current row of the manifold - `beamIndices` : the resulting `Set` of beam indices from the previous row -We zip the `row` with its index and then filter it with two conditions : +We use the fact that a `String` acts like an `Array[Char]`. We zip it with its index and filter it with two conditions : - A beam is travelling at this index - There is a splitter at this index @@ -85,7 +85,7 @@ We zip the `row` with its index and then filter it with two conditions : The function returns the list of indices as we don't need anything else. ```scala -private def findSplitIndices(row: Array[Char], beamIndices: Set[Int]): List[Int] = +private def findSplitIndices(row: String, beamIndices: Set[Int]): List[Int] = row .zipWithIndex .filter: (location, i) => @@ -207,12 +207,12 @@ val updatedBeamTimelines = ## Final code ```scala -type Manifold = Array[Array[Char]] +type Manifold = Array[String] private def parse(input: String): Manifold = - input.split("\n").map(_.toArray) + input.split("\n") -private def findSplitIndices(row: Array[Char], beamIndices: Set[Int]): List[Int] = +private def findSplitIndices(row: String, beamIndices: Set[Int]): List[Int] = row .zipWithIndex .filter: (location, i) => From 151b6981e1e0b49668cb55c6a930d5072c5330fd Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 21:03:34 +0100 Subject: [PATCH 6/7] Fix a couple typos --- docs/2025/puzzles/day07.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index 6a91b164f..9d81d7ad5 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -22,7 +22,7 @@ Parsing the input is quite straighforward. First let's define a type alias so th type Manifold = Array[String] ``` -As the input is a multiline `String` with each line representing a row of the manifold, we simply split by lines: +As the input is a multiline `String` with each line representing a row of the manifold, we simply split it by lines: ```scala private def parse(input: String): Manifold = @@ -33,7 +33,7 @@ private def parse(input: String): Manifold = 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. -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 : +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: - Count the number of splitters hit by a beam - Update the positions of the beam for the next row From 6e362bc8f5ffffe28a75058f08e29f300381d62e Mon Sep 17 00:00:00 2001 From: Antoine Amiguet Date: Sun, 7 Dec 2025 21:53:36 +0100 Subject: [PATCH 7/7] Fix variable names --- docs/2025/puzzles/day07.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/2025/puzzles/day07.md b/docs/2025/puzzles/day07.md index 9d81d7ad5..9ccf5958b 100644 --- a/docs/2025/puzzles/day07.md +++ b/docs/2025/puzzles/day07.md @@ -172,7 +172,7 @@ Since we don't care about the past timelines but only the current positions: if Overall this is implemented with: ```scala -val splittedTimelines = +val splitTimelines = splitIndices .flatMap: i => // splitting a timeline @@ -194,7 +194,7 @@ From the previous beam timelines map we finally compute the new beam timelines ` ```scala val updatedBeamTimelines = - splittedTimelines + splitTimelines .foldLeft(beamTimelines): (bm, s) => bm.updatedWith(s._1): // adding a new key