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