day11_part1.py (3830B)
1 #!/usr/bin/env python 2 """Advent of Code 2021, day 11 (part 1): Flashing octopuses 3 For part 1, we're simulating bioluminescent octopuses that 'flash' when they 4 reach a threshold 'energy level'.""" 5 6 # Algorithm for simulating one 'step' of the octopuses (this is basically 7 # breadth-first search): 8 # initialize "increments" to 1 9 # initialize "already flashed" to false 10 # while any increments are 1 11 # add increments to energy 12 # set increments to 0 13 # set flashing now to energy > 9 and not already flashed 14 # set already flashed to already flashed or flashing now 15 # for flashing octopus in flashing now 16 # increase neighbor's increments by 1 17 # Note: these functions kinda ignore Python's pass-by-reference behavior, 18 # they're updating numpy arrays in place and also returning them, but that's 19 # not causing any problems here. 20 21 from typing import Tuple 22 import itertools 23 import functools 24 import numpy as np 25 from utils import get_puzzle_input, convert_input_to_array 26 27 EXAMPLE_INPUT = \ 28 """5483143223 29 2745854711 30 5264556173 31 6141336146 32 6357385478 33 4167524645 34 2176841721 35 6882881134 36 4846848554 37 5283751526 38 """ 39 40 def increment_neighbors(increments: np.ndarray, 41 row: int, col: int) -> np.ndarray: 42 """Increase the value of the given `increments` 2d array by 1 in the 43 8-connected neighborhood of the element at the given row and column""" 44 # Note that this doesn't check if `row` and `col` are valid, but it 45 # will fail silently (possibly updating elements at the edges 46 for row_offset, col_offset in itertools.product(range(-1, 2), range(-1, 2)): 47 if row_offset != 0 or col_offset != 0: 48 row_idx = row + row_offset 49 col_idx = col + col_offset 50 if 0 <= row_idx < increments.shape[0] and \ 51 0 <= col_idx < increments.shape[1]: 52 increments[row_idx, col_idx] += 1 53 return increments 54 55 def simulate_step(energy_levels: np.ndarray, 56 flashes: int = 0) -> Tuple[np.ndarray, int]: 57 """For the given 2d array of energy levels and the number of flashes 58 detected so far, simulate one step of the flashing octopuses and return a 59 tuple containing the new energy levels and an updated number of flashes""" 60 increments = np.ones_like(energy_levels) 61 already_flashed = np.zeros_like(energy_levels, dtype=bool) 62 63 while increments.any(): 64 # Apply the current set of increments 65 energy_levels += increments 66 increments[:,:] = 0 67 # Figure out who's flashing now and update who's flashed so far 68 flashing_now = (energy_levels > 9) & (~already_flashed) 69 already_flashed |= flashing_now 70 # Update neighbors' energy levels (i.e., determine who will be 71 # incremented on the next step through the loop) 72 for row, col in zip(*np.where(flashing_now)): 73 increments = increment_neighbors(increments, row, col) 74 75 # Reset the flashing octopuses' energy levels to 0 76 energy_levels[already_flashed] = 0 77 return (energy_levels, 78 flashes + already_flashed.sum()) 79 80 def run_simulation(energy_levels: np.ndarray, steps: int) -> int: 81 """Run the simulation from the initial 2d array of energy levels for the 82 given number of steps, and return the number of flashes detected""" 83 return functools.reduce( 84 lambda x, y: simulate_step(*x), 85 range(steps), 86 (energy_levels, 0) 87 )[1] 88 89 def solve_puzzle(input_string: str) -> int: 90 """Return the numeric solution to the puzzle""" 91 return run_simulation(convert_input_to_array(input_string), 100) 92 93 def main() -> None: 94 """Run when the file is called as a script""" 95 assert solve_puzzle(EXAMPLE_INPUT) == 1656 96 print("Number of flashes after 100 steps:", 97 solve_puzzle(get_puzzle_input(11))) 98 99 if __name__ == "__main__": 100 main()