Advent of Code in Kotlin 2021 - Day 4
This is part 4 of a series, so if you haven't read the previous parts, start here.
Today's puzzle has you simulating a game of bingo against a giant squid. You are given a list of numbers and a set of boards.
For part 1 you have to determine the first board to win and calculate a score by taking the sum of all unmarked numbers on that board and multiplying it by the final number. Part 2 asks for the score of the last board to win.
The first order of business is parsing the data into a structure. I already have a helper that parses chunks separated by a blank line, so I use that here.
fun main() {
val input = inputChunks(2021, 4)
val numbers = input.first().split(",").map(String::toInt)
val boards = input.drop(1)
.map {
it
.split(System.lineSeparator())
.map { row -> row.split(" ").filter(String::isNotBlank).map(String::toInt) }
}
.map(::BingoBoard)
}
In the BingoBoard class, I store the count of marked numbers in each row / column in an array. For checking if the board is complete, I only need the count of marked numbers, not the numbers themselves. I also store the position of each number in a map.
This allows the markNumber function to know which counts to update without having to iterate over the board. Once one of the counts reaches 5 (the size of the board), it will be marked as complete.
private class BingoBoard(private val rows: List<List<Int>>) {
var isComplete: Boolean = false
val numRows = rows.size
val numCols = rows.first().size
private val markedNumbers = mutableSetOf<Int>()
private val numPositions = rows.flatMapIndexed { rowIndex, row ->
row.mapIndexed { colIndex, num ->
num to Pair(rowIndex, colIndex)
}
}.toMap()
val unmarkedNumbers: List<Int>
get() = rows.flatten().minus(markedNumbers)
private val markedRows = MutableList(numRows) { 0 }
private val markedCols = MutableList(numCols) { 0 }
fun markNumber(number: Int) {
markedNumbers.add(number)
val (row, col) = numPositions[number] ?: return
markedRows[row] = markedRows[row] + 1
markedCols[col] = markedCols[col] + 1
if (markedRows[row] == numRows || markedCols[col] == numCols) isComplete = true
}
}
Now we can iterate over the numbers, updating each board as we go (skipping completed boards). When a board is completed we record the score. This seemed more convenient than iterating over a full board before moving on to the next since it means we get the scores in the right order.
private fun getScores(boards: List<BingoBoard>, numbers: List<Int>): List<Int> {
var scores = emptyList<Int>()
numbers.forEach { number ->
boards.forEach { board ->
if (!board.isComplete) {
board.markNumber(number)
if (board.isComplete) {
scores = scores + board.unmarkedNumbers.sum() * number
}
}
}
}
return scores
}
All that's left is to calculate all the scores, which gives us the answer for both part 1 and part 2.
fun main() {
val input = inputChunks(2021, 4)
val numbers = input.first().split(",").map(String::toInt)
val boards = input.drop(1)
.map {
it
.split(System.lineSeparator())
.map { row -> row.split(" ").filter(String::isNotBlank).map(String::toInt) }
}
.map(::BingoBoard)
val scores = getScores(boards, num``bers)
println(scores.first())
println(scores.last())
}
Hacking together an answer for this one was reasonably quick, but it took a little while to polish it into something reasonably efficient. It wasn't really necessary since it's only day 4 and the input is quite small, but if experience is anything to go by later days won't be quite as forgiving.