day08_part1.py (5698B)
1 #!/usr/bin/env python 2 """Advent of Code 2021, day 8 (part 1): decode the scrambled seven-segment 3 displays""" 4 5 # This is a fun puzzle. I sat down with pen and paper and proved to myself that 6 # you can deduce the mapping from scrambled segments to the full set of digits 7 # using just the information provided. It looks like this: 8 # * If 2 segments are lit, that's '1' 9 # * If 3 segments are lit, that's '7' 10 # * If 4 segments are lit, that's '4' 11 # * If 7 segments are lit, that's '8' 12 # (So far, this was given in the puzzle prompt, nothing new yet) 13 # * If 5 segments are lit, it's '2', '3', or '5' 14 # * If 6 segments are lit, it's '0', '6', or '9' 15 # (Okay, that's not useful... yet) 16 # * If 5 segments are lit and they include all the segments in '7', that's '3' 17 # * If 6 segments are lit and they don't include all the segments in '7', 18 # that's '6' 19 # * If 6 segments are lit and they include the union of the segments comprising 20 # '3' and '4', that's '9' 21 # * If 6 segments are lit and it's not a '6' or '9', that's '0' 22 # * If 5 segments are lit and it is missing only one of the segments from '6', 23 # that's '5' 24 # * If 5 segments are lit and it's not a '5' or a '3', that's '2' 25 # So right now in part 1, it's possible to find the four digits displayed for 26 # each line of puzzle input! But... 27 # I'm not going to implement it yet, because "good programmers are lazy", and 28 # maybe part 2 will throw me for a loop? Regardless, figuring this out has 29 # influenced my decision about how to represent the data; e.g., we're doing a 30 # lot of set operations, so I'm going to use sets for everything, and since 31 # it's easy to imagine wanting to use these sets of lit segments as dictionary 32 # keys, we'll use immutable `frozenset`s specifically (which can be `dict` 33 # keys). 34 35 36 import pandas as pd 37 from utils import get_puzzle_input, convert_lines_to_series 38 39 EXAMPLE_INPUT = \ 40 """be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe 41 edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc 42 fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg 43 fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb 44 aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea 45 fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb 46 dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe 47 bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef 48 egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb 49 gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce 50 """ 51 52 def convert_input_to_df(input_string): 53 """Convert the puzzle input to a five-column df: the first a string of 54 unique segment sets, and the next four `frozenset` objects representing the 55 display digits""" 56 display_df= ( 57 convert_lines_to_series(input_string) 58 .str.rsplit(' ', 4, expand=True) 59 .rename(columns=dict(enumerate(('segments', 'digit_1', 'digit_2', 60 'digit_3', 'digit_4')))) 61 .assign(segments=lambda x: x['segments'].str.replace(' |', '', 62 regex=False)) 63 ) 64 display_df.loc[:, 'digit_1':'digit_4'] = ( 65 display_df.loc[:, 'digit_1':'digit_4'].applymap(frozenset) 66 ) 67 return display_df 68 69 def create_mapper_from_segments(segment_sequence): 70 """Given the observed sequence of segment sets (which isn't actually used 71 for anything here), return a function that maps a `frozenset` of segments 72 to a digit""" 73 # Another example of getting ready for what part 2 might look like: this is 74 # more complicated than necessary but if we do need to decode every line 75 # then this function is one of the few that would need to be changed. 76 def part_1_mapper(segments): 77 """This is the simple mapping function that we need for part 1, that 78 identifies the digits '1', '4', '7', and '8' based on the number of 79 segments that are lit. Returns the matching digit as a string or `None` 80 if its not one of these""" 81 return ( 82 {2: '1', 3: '7', 4: '4', 7: '8'} 83 .get(len(segments), None) 84 ) 85 return part_1_mapper 86 87 def apply_mappers_to_digits(display_df, mapper_creator): 88 """Given a df that contains the unique segment sequence and four columns of 89 digits, use the `mapper_creator` function to make a mapping function for 90 each row of segment sequences and apply that function to the digits from 91 the same row, then return a df with the digits""" 92 # I don't love how this is coded, not one bit 93 digits_dict = {} 94 for col in [f"digit_{i}" for i in range(1, 5)]: 95 digits_dict[col] = [f(x) for f, x in \ 96 zip(display_df['segments'].map(mapper_creator), 97 display_df[col])] 98 return pd.DataFrame(digits_dict) 99 100 def count_visible_digits(digits_df): 101 """Given a four-column df of digit displays, return the number of 'visible' 102 digits (one of '1', '4', '7', or '8') present""" 103 return int(digits_df.count().sum()) 104 105 def solve_puzzle(input_string): 106 """Return the numeric solution to the puzzle""" 107 return count_visible_digits( 108 apply_mappers_to_digits( 109 convert_input_to_df(input_string), 110 create_mapper_from_segments 111 ) 112 ) 113 114 def main(): 115 """Run when the file is called as a script""" 116 assert solve_puzzle(EXAMPLE_INPUT) == 26 117 print("Number of easily decoded digits:", 118 solve_puzzle(get_puzzle_input(8))) 119 120 if __name__ == "__main__": 121 main()