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