advent_of_code_2021

My attempts to work through the 2021 Advent of Code problems.
git clone https://git.eamoncaddigan.net/advent_of_code_2021.git
Log | Files | Refs | README | LICENSE

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()