Day 7: Laboratories

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

  • GiantTree@feddit.org
    link
    fedilink
    arrow-up
    1
    ·
    9 months ago

    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)
                }
            }
        }
    }