AoC 2021 - Day 11

Today’s challenge is available here.

Octopuses flash when their energy levels reach above 9 and it causes their neighbors to increase their levels by 1 too? This sounds like a perfect job for some kind of a matrix operation…

Loading data:

def load():
    with open('../.input/day11') as f:
        lines = list(map(str.strip, f.readlines()))
    return np.array([int(num) for line in lines for num in line]).reshape((-1, len(lines)))

In this challenge I decided to try my hand at using convolution matrix to solve this problem. First I had to create a function that would return an array containing octopuses and their neighbors. For this I used the following code:

from numpy.lib.stride_tricks import as_strided

def windows(target, shape=(3, 3), stride: int = 1):
    target = np.pad(target, 1, 'constant', constant_values=0)
    (t_h, t_w), (w_h, w_w) = target.shape, shape
    out_shape = (
      1 + (t_h - w_h) // stride,
      1 + (t_w - w_w) // stride,
      w_h, w_w
    )
    out_strides = (
      target.strides[0] * stride,
      target.strides[1] * stride,
      target.strides[0], target.strides[1]
    )
    return as_strided(target, shape=out_shape, strides=out_strides)

The function as_strided is a pretty esoteric function, which operates directly on memory, which also means a lot can go very wrong when using it.

Here I used this function to calculate how the energy levels would progress through the time:

def step(input: np.ndarray):
    current = input + np.ones(input.shape, dtype=np.int32)
    flashed = np.zeros(input.shape, dtype=np.bool8)

    while np.any(now_flashed := ((current * ~flashed) > 9)):
        convolution = np.tensordot(
            windows(now_flashed.astype(np.int32)),
            np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]),
            axes=((2, 3), (0, 1))
        )
        flashed |= now_flashed
        current = current + convolution * ~flashed

    return np.where(current > 9, 0, current), flashed

I used a convolution matrix of the following shape: [111101111] \begin{bmatrix} 1 & 1 & 1 \\\\ 1 & 0 & 1 \\\\ 1 & 1 & 1 \end{bmatrix}

Task 1

Here we sum all the flashes that happened in all of the iterations with np.sum:

def solve1() -> int:
    input = load()
    flash_count = 0
    for _ in range(100):
        input, flashed = step(input)
        flash_count += np.sum(flashed)
    return flash_count

Task 2

Here we check when all the octopuses flashed with np.all:

def solve2() -> int:
    input = load()

    steps = 0
    while (steps := steps + 1):
        input, flashed = step(input)
        if np.all(flashed):
            return steps