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

day09_part2.py (3043B)


      1 #!/usr/bin/env python
      2 """Advent of Code 2021, day 9 (part 2): find the three largest "basins" in the
      3 heightmap"""
      4 
      5 # I was wrong about convolution, but I'm still glad I'm working with a 2D
      6 # array. I'm not convinced that working from the 'low points' to find the size
      7 # of the basins is the most algorithmically efficient approach, but it's
      8 # conceptually simple: for each of our low points (226, in my case, and 4 in
      9 # the case of the example input), we'll perform depth first search to determine
     10 # which nodes (here, positions in the 2D array) belong to that basin and also
     11 # find its size. Doing depth first search in "pure" functional languages is
     12 # acknowledged as a hard problem, so I'm not going to even bother trying in
     13 # Python--I'll freely take advantage of side effects and not feel even a tiny
     14 # bit of guilt for it (okay, maybe a tiny bit).
     15 
     16 import numpy as np
     17 from day09_part1 import (EXAMPLE_INPUT,
     18                          convert_input_to_array,
     19                          find_low_points)
     20 from utils import get_puzzle_input
     21 
     22 def find_basin_sizes(heightmap):
     23     """Return a sorted list of basin sizes. Note that the number of unique
     24     basins won't necessarily equal the number of low points (two local minima
     25     can easily share a basin), and since we don't need to worry about
     26     preserving the mapping between low points and basins we won't even try"""
     27     low_points = find_low_points(heightmap)
     28     visitmap = np.zeros_like(low_points)    # 2D bool array initialized False
     29 
     30     def visit_node_and_get_size(row, col):
     31         """Depth first search: mark the `visitmap` visited at the current row
     32         and column, and return the size (so far) of the basin. Returns 0 when
     33         encountering a basin "ridge" (a location where `heightmap[row, col] ==
     34         9`, a previously visited location (which means that some low points
     35         will return sizes of 0 when they are part of previously-explored
     36         basins), or a node beyond the edge of `heightmap`."""
     37         if (row < 0 or row >= heightmap.shape[0]) or \
     38                 (col < 0 or col >= heightmap.shape[1]) or \
     39                 visitmap[row, col] or \
     40                 heightmap[row, col] == 9:
     41             return 0
     42         visitmap[row, col] = True
     43         return 1 + \
     44             visit_node_and_get_size(row-1, col) + \
     45             visit_node_and_get_size(row+1, col) + \
     46             visit_node_and_get_size(row, col-1) + \
     47             visit_node_and_get_size(row, col+1)
     48 
     49     basin_sizes = [visit_node_and_get_size(r, c) for \
     50                    r, c in zip(*np.where(low_points))]
     51 
     52     return sorted(basin_sizes, reverse=True)
     53 
     54 def solve_puzzle(input_string):
     55     """Return the numeric solution to the puzzle"""
     56     return np.prod(find_basin_sizes(convert_input_to_array(input_string))[0:3])
     57 
     58 def main():
     59     """Run when the file is called as a script"""
     60     assert solve_puzzle(EXAMPLE_INPUT) == 1134
     61     print("Product of the sizes of the largest three basins:",
     62           solve_puzzle(get_puzzle_input(9)))
     63 
     64 if __name__ == "__main__":
     65     main()