Advent of Code 2021: Day 4

Search for a command to run...

No comments yet. Be the first to comment.
In this series we solve programming challenges from the Advent of Code 2021 using JavaScript.
This blog post is fifth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 5. The challenges were about finding overlapping points given a set of lines. All solutions are in this GitHub repository....
This blog post is fourteenth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 14. This challenge was about optimizing an algorithm that generates exponentially larger and larger strings. I found ...

This blog post is thirteenth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 13. All solutions are in this GitHub repository. Solving Part One Given a list of x, y coordinates representing dots ...

This blog post is twelfth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described on Day 12. All solutions are in this GitHub repository. Solving Part One Given a set of connections between underground caves, ...

This blog post is eleventh in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 11. This day had many similarities to Day 9, and I solved the challenge using similar methods. All solutions are in thi...

This blog post is the tenth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 10. All solutions are in this GitHub repository. Solving Part One Given a set of lines with opening and closing bracke...

This blog post is fourth in the Advent of Code 2021 series and shows a JavaScript-based solution to the problem described in Day 4. The stories never stop surprising me. This one, in particular, is about playing bingo with a giant squid! Consider me entertained 😎
All solutions are in this GitHub repository.
We need to find the first winning bingo board and calculate the board's score. A winning bingo board contains an entire row or column of extracted random numbers.
The strategy of breaking down the problem into smaller, more manageable chunks worked well for day 3, so let's explore the steps for solving the fourth challenge:
The input contains the extracted random numbers on the first line, followed by 5 x 5 boards of numbers separated by empty lines. We can extract the bingo numbers and boards as strings using the double Newline separator.
const [bingoNumbersAsString, ...boardsAsString] = data.split('\n\n')
We can extract the array of bingo numbers by using the , as a separator and parsing each number.
const bingoNumbers = bingoNumbersAsString
.split(',')
.map((numberAsString) => Number(numberAsString))
Next, we must store board numbers in a data structure. I opted to keep each board as an array of 25 objects containing the number and a marked boolean.
{
number: 4,
marked: false,
}
The marked property is set to false by default and true whenever the number matches the played bingo number. A matrix with objects of the same structure would have worked well too. However, I thought it would be easier to traverse an array to find the matching bingo number rather than a matrix.
Here is the mapping of the input to the desired data structure.
const boards = boardsAsString.map((boardAsString) =>
boardAsString
.replaceAll('\n', ' ')
.split(' ')
.filter(Boolean)
.map((numberAsString) => ({
number: Number(numberAsString),
marked: false,
}))
)
All new lines are concatenated separated by a space using .replaceAll('\n', ' '), then .split(' ') creates an array using the space character as a separator. Since some of the numbers have additional spaces, some of the items in the array are empty strings. The .filter(Boolean) function removes all the empty strings from the array. Finally, each number is parsed and mapped into the data structure.
We can use the forEach function to iterate through bingo numbers and boards. However, forEach goes through all elements, whereas we should stop whenever we find a winning board. The some function returns true when finding a winning board.
bingoNumbers.some((bingoNumber) => {
return boards.some((board) => {
// mark the bingo number on the board
// determine if it is a winning board
return isWinner
})
To find and mark the bingo number on a board we can use the find function and set the marked property to true when the board contains the number.
bingoNumbers.some((bingoNumber) => {
return boards.some((board) => {
const bingoNumberOnBoard = board.find((item) => item.number === bingoNumber)
if (!bingoNumberOnBoard) {
return
}
bingoNumberOnBoard.marked = true
// determine if it is a winning board
return isWinner
})
To determine whether the board is winning, we must check if the row or column of the marked number is now complete. Both checks require the index of the bingoNumberOnBoard.
const bingoNumberIndexOnBoard = board.findIndex(item) => item === bingoNumberOnBoard)
Given that each board is 5 x 5, we can use the index of the bingo number in the array to determine indices of the elements of the row. Each board has the following indices:
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
We can determine the indices of the row of the bingo number with the formula rowIndex * 5 + columnIndex. The rowIndex is the quotient of the bingo number index divided by 5 and the column indices are always 0, 1, 2, 3, 4 . For example, for the index 13, the row index is 13 ÷ 5 which is 2, thus the indices are 2 * 5 + 0 (10), 2 * 5 + 1 (11), 2 * 5 + 2 (12), 2 * 5 + 3 (13) and 2 * 5 + 4 (14).
Then, we only need to count how many marked elements there are on the row and return true if there are 5 marked elements and false otherwise.
const checkRow = (board, bingoNumberIndexOnBoard) => {
const rowIndex = Math.floor(bingoNumberIndexOnBoard / 5)
const reducer = (markedTotal, columnIndex) => {
const boardIndex = rowIndex * 5 + columnIndex
if (board[boardIndex].marked) {
return markedTotal + 1
}
return markedTotal
}
const markedTotal = [0, 1, 2, 3, 4].reduce(reducer, 0)
return markedTotal === 5
}
In the solve function we can call the checkRow function like this:
const isRow = checkRow(board, bingoNumberIndexOnBoard)
The column check is calculated in a similar manner to the row, except we determine the indices slightly differently. The row indices are always 0, 1, 2, 3, 4 and the column index is the remainder of the bingo number index divided by 5.
const checkColumn = (board, bingoNumberIndexOnBoard) => {
const columnIndex = bingoNumberIndexOnBoard % 5
const reducer = (markedTotal, rowIndex) => {
const boardIndex = rowIndex * 5 + columnIndex
if (board[boardIndex].marked) {
return markedTotal + 1
}
return markedTotal
}
const markedTotal = [0, 1, 2, 3, 4].reduce(reducer, 0)
return markedTotal === 5
}
In the solve function we can call the checkColumn function like this:
const isColumn = checkColumn(board, bingoNumberIndexOnBoard)
The score is calculated by summing all the unmarked numbers and multiplying the sum with the bingo number. We can use the reduce function as follows:
const calculateScore = (board, bingoNumber) => {
const boardScore = board.reduce((boardScore, item) => {
if (item.marked) {
return boardScore
}
return boardScore + item.number
}, 0)
return boardScore * bingoNumber
}
In the solve function we can call the calculateScore function if we found a winning board:
const isWinner = isRow || isColumn
if (isWinner) {
score = calculateScore(board, bingoNumber)
}
You can see the final solution for part one on my GitHub repository.
We need to find the last winning bingo board. We can reuse most of the code defined in part one, such as reading input, checking whether a row or column is complete and calculating the score.
Here is how we could break down the differences:
The main difference from part one is that instead of stopping after finding the first winning board, we must go through all bingo numbers until all boards have at least one complete row or column. Thus, forEach works well in this case.
bingoNumbers.forEach((bingoNumber) => {
boards.forEach((board) => {
// ...
})
})
We must keep track of winning boards to prevent continued markings and secondary winning. We can add a won property to each board.
boards = boards.map((board) => ({
won: false,
board
}))
We must refactor the code to account for this data structure change. In addition, we skip marking numbers on winning boards and set the won property to true whenever we find a new winning board.
let lastWinningScore = 0
bingoNumbers.forEach((bingoNumber) => {
boards.forEach((boardObject) => {
const { board, won } = boardObject
if (won) {
return
}
// ...
if (isWinner) {
boardObject.won = true
lastWinningScore = calculateScore(board, bingoNumber)
}
})
})
You can see the final solution for part two on my GitHub repository.
We wrote some code that simulates playing bingo. We determined the first and last winning boards, and we got to use a variety of JavaScript array functions, such as some, forEach, reduce, map, etc. As usual, I had fun with this challenge, and I am looking forward to day 5.