AoC 2021 - Day 4

Today’s problem is Day 4. This time the challenge is inspied by the game called Bingo. We get a vector of number and a number of bingo boards. We need to find the board that wins first and last, then we need to calculate score for it.

Loading data from file into NumPy arrays:

def loader_np() -> np.ndarray:
    with open('../.input/day04', 'r') as f:
        data = f.read().splitlines()

    vector = np.array([int(x) for x in data[0].split(",")])
    tensor = np.array([
        np.array(list(map(np.int32, map(str.split, board))))
        for board
        in [data[i:i+5] for i in range(2, len(data), 6)]
    ])
    return vector, tensor

Task 1

Here we need to find the first winning board. I used the np.isin function to find the mask for all board, for example given vector [1, 2, 3, 4, 5] and matrix [[1, 2, 3], [4, 5, 6]] we get [[True, True, True], [True, True, False]]. The vector changes size through slicing, with each iteration of the outer loop - it simulates drawing a new number. The inner for loop iterates through all boards (matrices), there we check if on any axis there’s a full row or column of matching numbers. If there’s a match we calculate the score.

def solve1() -> int:
    vector, tensor = loader_np()
    for scale in range(1, len(vector)+1):
        for matrix in tensor:
            mask = np.isin(matrix, vector[:scale])
            if mask.all(axis=0).any() or mask.all(axis=1).any():
                return (matrix * ~mask).sum() * vector[scale-1]

The score is calculated by taking a dot product of the matrix with the negation of the mask (which eliminates unmarked numbers), summing the remaining values and multiplying by the value of the drawn number.

score=vdrawni=1nj=1mboard[i,j]¬mask[i,j] \text{score} = v_{drawn} \cdot \sum_{i=1}^{n} \sum_{j=1}^{m} \text{board}[i,j] \cdot \neg \text{mask}[i,j]

Task 2

This is analogous to the first task, but we need to find the last winning board. I iterate from the end of the vector. The inner loop is similar to the one in the first task, just needed to find when the first board doesn’t match and then calculate another mask for the round afterwards (scale+1).

def solve2() -> int:
    vector, tensor = loader_np()
    for scale in range(len(vector)+1, 0, -1):
        for matrix in tensor:
            mask = np.isin(matrix, vector[:scale])
            if not mask.all(axis=0).any() and not mask.all(axis=1).any():
                prev_mask = np.isin(matrix, vector[:scale+1])
                return (matrix * ~prev_mask).sum() * vector[scale]