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

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