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_part2.py (5026B)


      1 #!/usr/bin/env python
      2 """Advent of Code 2021, day 8 (part 2): decode all of the scrambled
      3 seven-segment 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 '2' or a '3', that's '2'
     25 # It's part 2, so apparently we're doing it!
     26 
     27 from day08_part1 import (EXAMPLE_INPUT,
     28                          convert_input_to_df,
     29                          apply_mappers_to_digits)
     30 from utils import get_puzzle_input
     31 
     32 def create_mapper_from_segments(segment_sequence):
     33     """Given the observed sequence of segment sets (which isn't actually used
     34     for anything here), return a function that maps a `frozenset` of segments
     35     to a digit"""
     36 
     37     # This approach isn't efficient (or elegant) at all, but I don't think this
     38     # function is being run enough times, on a long enough list, to justify the
     39     # code complexity for a more efficient/elegant approach
     40     segment_sets = [frozenset(i) for i in segment_sequence.split(' ')]
     41     digits_to_segments = {}
     42 
     43     # Step 1 of building a segment to digit map: find the mapping to '1', '7',
     44     # '4', and '8'
     45     for i, seg in enumerate(segment_sets):
     46         if len(seg) == 2:
     47             digits_to_segments['1'] = segment_sets.pop(i)
     48             break
     49     for i, seg in enumerate(segment_sets):
     50         if len(seg) == 3:
     51             digits_to_segments['7'] = segment_sets.pop(i)
     52             break
     53     for i, seg in enumerate(segment_sets):
     54         if len(seg) == 4:
     55             digits_to_segments['4'] = segment_sets.pop(i)
     56             break
     57     for i, seg in enumerate(segment_sets):
     58         if len(seg) == 7:
     59             digits_to_segments['8'] = segment_sets.pop(i)
     60             break
     61 
     62     # Step 2: using '7' alone, find '3' and '6'
     63     for i, seg in enumerate(segment_sets):
     64         if len(seg) == 5 and not digits_to_segments['7'] - seg:
     65             digits_to_segments['3'] = segment_sets.pop(i)
     66             break
     67     for i, seg in enumerate(segment_sets):
     68         if len(seg) == 6 and (digits_to_segments['7'] - seg):
     69             digits_to_segments['6'] = segment_sets.pop(i)
     70             break
     71 
     72     # Step 3: Using '4', '3', and '6', find '9', and '5'
     73     for i, seg in enumerate(segment_sets):
     74         if len(seg) == 5 and len(digits_to_segments['6'] - seg) == 1:
     75             digits_to_segments['5'] = segment_sets.pop(i)
     76             break
     77     for i, seg in enumerate(segment_sets):
     78         if len(seg) == 6 and \
     79                 seg == digits_to_segments['3'] | digits_to_segments['4']:
     80             digits_to_segments['9'] = segment_sets.pop(i)
     81             break
     82 
     83     # Step 4 (final): Using '3', '6', '2', and '9', find '0' and '2'
     84     for i, seg in enumerate(segment_sets):
     85         if len(seg) == 5:
     86             digits_to_segments['2'] = segment_sets.pop(i)
     87             break
     88     digits_to_segments['0'] = segment_sets.pop()
     89 
     90     segments_to_digits = {v:k for k, v in digits_to_segments.items()}
     91 
     92     def part_2_mapper(segments):
     93         """Just a closure wrapping the dictionary (hackily) built above"""
     94         return segments_to_digits.get(segments)
     95     return part_2_mapper
     96 
     97 def convert_single_digits_to_integers(digits_df):
     98     """Given a four-column df of digit displays, return a Series consisting of
     99     a the single integer value obtained across each row"""
    100     # Can this be done with `apply` somehow? This feels needlessly inelegant
    101     return (
    102         digits_df['digit_1']
    103         .add(digits_df['digit_2'])
    104         .add(digits_df['digit_3'])
    105         .add(digits_df['digit_4'])
    106         .astype(int)
    107     )
    108 
    109 def solve_puzzle(input_string):
    110     """Return the numeric solution to the puzzle"""
    111     return int(
    112         convert_single_digits_to_integers(
    113             apply_mappers_to_digits(
    114                 convert_input_to_df(input_string),
    115                 create_mapper_from_segments
    116             )
    117         )
    118         .sum()
    119     )
    120 
    121 def main():
    122     """Run when the file is called as a script"""
    123     assert solve_puzzle(EXAMPLE_INPUT) == 61229
    124     print("Sum of all decoded displays:",
    125           solve_puzzle(get_puzzle_input(8)))
    126 
    127 if __name__ == "__main__":
    128     main()