day05_part1.py (3743B)
1 #!/usr/bin/env python 2 """Advent of Code 2021, day 5 (part 1): the hydrothermal vents. Given the start 3 and end coordinates of lines of vents, get information about the field.""" 4 5 # For Part 1, we don't need to operate on any 2D representation of the field 6 # directly, so I won't bother building one. Again I'm sticking to pandas and 7 # doing as much as I can with DFs. 8 9 import numpy as np 10 import pandas as pd 11 from utils import get_puzzle_input 12 13 def convert_input_to_df(input_string): 14 """The input is a series of start and ending coordinates. Parse these and 15 return a df with the columns `x1`, `y1`, `x2`, `y2`""" 16 return ( 17 pd.Series([x for x in input_string.split('\n') if x]) 18 .str.split(',| -> ', n=4, expand=True) 19 .rename(columns={0: 'x1', 1: 'y1', 2: 'x2', 3: 'y2'}) 20 .astype(np.int32) 21 ) 22 23 def find_points_x(x_1, y_1, x_2, y_2): 24 """Given a line segment's start and end coordinates, return a numby array 25 of x coordinates for the points along segment""" 26 # This is easy because we're only dealing with horizontal and vertical 27 # lines (for now) 28 if x_1 == x_2: 29 return np.repeat(x_1, abs(y_2 - y_1) + 1) 30 return np.arange(min(x_1, x_2), max(x_1, x_2)+1) 31 32 def find_points_y(x_1, y_1, x_2, y_2): 33 """Given a line segment's start and end coordinates, return a numby array 34 of x coordinates for the points along segment""" 35 return find_points_x(x_1=y_1, y_1=x_1, x_2=y_2, y_2=x_2) 36 37 def find_line_points(endpoints_df): 38 """Given a df with the start and end coordinates of the line segments, add 39 new columns, `x` and `y`, that give the coordinates of all points covered 40 by the line (note that line data will be dropped but indices are 41 retained)""" 42 # In Part 1 (I sense that Part 2 will be different), we only transform the 43 # horizontal and verticl line segments. We'll remove the diagonal line 44 # segments from the df. 45 return ( 46 endpoints_df 47 .loc[(endpoints_df['x1'] == endpoints_df['x2']) | 48 (endpoints_df['y1'] == endpoints_df['y2'])] 49 .assign(x=lambda df: [find_points_x(*x[1].to_list()) \ 50 for x in df[['x1', 'y1', 'x2', 'y2']].iterrows()], 51 y=lambda df: [find_points_y(*x[1].to_list()) \ 52 for x in df[['x1', 'y1', 'x2', 'y2']].iterrows()]) 53 .explode(['x', 'y']) 54 .drop(columns=['x1', 'y1', 'x2', 'y2']) 55 ) 56 57 def count_point_coverage(points_df): 58 """Given a df with the `x` and `y` coordinates of all covered points, find 59 out how many times each point is covered (among points covered at least 60 once)""" 61 return ( 62 points_df 63 .groupby(['x', 'y']).size() 64 .reset_index() 65 .rename(columns={0: 'n'}) 66 ) 67 68 def count_danger_points(coverage_df): 69 """Given a df with the total coverage for each coordinate, count the number 70 of points with a coverage > 2""" 71 return len(coverage_df.loc[coverage_df['n'] > 1]) 72 73 def solve_puzzle(input_string): 74 """Return the numeric solution to the puzzle""" 75 return count_danger_points( 76 count_point_coverage( 77 find_line_points( 78 convert_input_to_df(input_string) 79 ) 80 ) 81 ) 82 83 if __name__ == "__main__": 84 assert solve_puzzle( 85 "\n".join(("0,9 -> 5,9", 86 "8,0 -> 0,8", 87 "9,4 -> 3,4", 88 "2,2 -> 2,1", 89 "7,0 -> 7,4", 90 "6,4 -> 2,0", 91 "0,9 -> 2,9", 92 "3,4 -> 1,4", 93 "0,0 -> 8,8", 94 "5,5 -> 8,2", 95 "\n"))) == 5 96 97 print("Number of points to avoid:", 98 solve_puzzle(get_puzzle_input(5)))