Kotlin
More grid stuff and two-dimensional problem solving, I like it!
The first part just requires extracting the numbers and operators, transposing the grid and summing/multiplying the numbers.
The second part is also not too hard. I just search for the numbers in the transposed grid, making sure to leave out the last column. That one might contain an operator (“+” or “*”). Remember it for later. If the entire row is made of spaces, we have finished parsing a math problem. Just remember to account for the last one! 😅
As with part one, just reduce the found problems and we’re done!
This solution requires the trailing spaces to be present in the input files. I had to disable an option in my IDE to prevent it from breaking my nice solution.
Code
class Day06 : AOCSolution {
override val year = 2025
override val day = 6
override fun part1(inputFile: String): String {
val worksheet = readResourceLines(inputFile)
// Arrange the problem in a grid and transpose it, so that the operation is the last element of each row
val problems = worksheet.map { line -> line.trim().split(spaceSplitRegex) }.toGrid().transposed()
val grandTotal = problems.rows().sumOf { line ->
// Map the all but the last element to a number
val numbers = line.mapToLongArray(0, line.lastIndex - 1, String::toLong)
// Extract the operation
val operation = line.last()[0]
// Call the correct reduction
// The "else" branch is needed for the compiler
when (operation) {
ADD -> numbers.sum()
MULTIPLY -> numbers.reduce { acc, value -> acc * value }
else -> 0
}
}
return grandTotal.toString()
}
override fun part2(inputFile: String): String {
val worksheet = readResourceLines(inputFile)
// In this part the problem is more complicated and dependent on the individual characters.
val charGrid = worksheet.map(CharSequence::toList).toCharGrid().transposed()
val numbers = mutableListOf<Long>()
val sb = StringBuilder(charGrid.width)
val problems = buildList {
// Begin with an empty operation
// Assume the operation will be set to a valid value
var operation = SPACE
for (y in 0 until charGrid.height) {
// Extract each row (transposed column)
sb.clear().append(charGrid[y])
// Find the bounds of the number
val numberOffset = sb.indexNotOf(SPACE)
if (numberOffset != -1) {
// A number was found, parse it and add it to the list.
val endIndex = sb.indexOfAny(STOP_CHARACTERS, numberOffset + 1)
val number = java.lang.Long.parseLong(sb, numberOffset, endIndex, 10)
numbers.add(number)
// Check whether there is an operation in the last column.
// IF so, that's the next relevant operation
val lastColumn = sb[sb.lastIndex]
if (lastColumn != SPACE) {
operation = lastColumn
}
} else {
// No number was found, that's the separator for two calculations.
// Finalize the collection and clear the numbers.
// `toLongArray` creates a neat copy of the Longs in the list.
add(Problem(operation, numbers.toLongArray()))
numbers.clear()
}
}
// Add the last remaining problem to the list
add(Problem(operation, numbers.toLongArray()))
}
// Reduce all problems to their solutions and sum them up.
val grandTotal = problems.sumOf { problem ->
when (problem.operation) {
ADD -> problem.numbers.sum()
MULTIPLY -> problem.numbers.reduce { acc, value -> acc * value }
else -> 0
}
}
return grandTotal.toString()
}
private companion object {
private const val ADD = '+'
private const val MULTIPLY = '*'
private const val SPACE = ' '
private val STOP_CHARACTERS = charArrayOf(SPACE, ADD, MULTIPLY)
@JvmRecord
@Suppress("ArrayInDataClass")
private data class Problem(val operation: Char, val numbers: LongArray)
}


Kotlin
Part 1 is easily solved by simulating the beams and modifying the grid in-place.
This does not work for part 2, however. Here I opted for a BFS algorithm, moving down row-by-row.
Similar to other solutions, I store the amount of incoming paths to a splitter, so that I can reference it later. Practically, the two beams from each splitter are representatives of all incoming beams. This reduces the search complexity by a lot!
The final count of timelines is the sum of the array that collects the incoming beam counts for each bottom-row of the diagram.
Code on GitHub
Code
class Day07 : AOCSolution { override val year = 2025 override val day = 7 override fun part1(inputFile: String): String { val diagram = readResourceLines(inputFile) .mapArray { line -> line.mapArray { char -> Cell.byChar(char) } } .toGrid() var count = 0 for (y in 1 until diagram.height) { for (x in 0 until diagram.width) { // Search for beam sources in the preceding row if (diagram[x, y - 1] in Cell.beamSourceTypes) { // Simulate the beam moving down when (diagram[x, y]) { Cell.Empty -> diagram[x, y] = Cell.Beam Cell.Splitter -> { // Split the beam and count this splitter diagram[x - 1, y] = Cell.Beam diagram[x + 1, y] = Cell.Beam count++ } else -> continue } } } } return count.toString() } override fun part2(inputFile: String): String { val diagram = readResourceLines(inputFile) .mapArray { line -> line.mapArray { char -> Cell.byChar(char) } } .toGrid() val height = diagram.height.toLong() val startPosition = diagram.positionOfFirst { it == Cell.Start } // Working stack of beam origin and split origin positions val stack = ArrayDeque<Pair<Position, Position>>() stack.add(startPosition to startPosition) // Splitter positions mapped to the count of timelines to them // Start with the start position and 1 timeline. val splitters = mutableMapOf<Position, Long>(startPosition to 1) // Keep track of all splitters for which new beams have been spawned already // Could be used to solve part 1, as well val spawnedSplitters = mutableSetOf<Position>() // Count the timelines per diagram exit, which is the bottom-most row val diagramExits = LongArray(diagram.width) while (stack.isNotEmpty()) { // Breadth first search for memorizing the amount of paths to a splitter val (beamOrigin, splitOrigin) = stack.poll() val originPathCount = splitters.getValue(splitOrigin) val nextPosition = beamOrigin + Direction.DOWN if (nextPosition.y < height) { if (diagram[nextPosition] == Cell.Splitter) { if (nextPosition !in spawnedSplitters) { // Only spawn new beams, if they weren't spawned already stack.add((nextPosition + Direction.LEFT) to nextPosition) stack.add((nextPosition + Direction.RIGHT) to nextPosition) spawnedSplitters.add(nextPosition) // Initialize the count splitters[nextPosition] = originPathCount } else { splitters.computeIfPresent(nextPosition) { _, v -> v + originPathCount } } } else { // Just move down stack.add(nextPosition to splitOrigin) } } else { diagramExits[nextPosition.x.toInt()] += originPathCount } } // Sum the count of timelines leading to the bottom row, i.e. leaving the diagram for each position return diagramExits.sum().toString() } private companion object { enum class Cell(val char: Char) { Start('S'), Empty('.'), Splitter('^'), Beam('|'); override fun toString(): String { return char.toString() } companion object { fun byChar(char: Char) = entries.first { it.char == char } val beamSourceTypes = arrayOf(Start, Beam) } } } }