#!/usr/bin/env python3 import sys from typing import DefaultDict, FrozenSet, List, Sequence, Tuple from collections import defaultdict import pprint def setstr(s: FrozenSet[str]) -> str: return "".join(sorted(s)) def part1(lines: Sequence[Tuple[Sequence[str], Sequence[str]]]): # Lazy solution lengths: DefaultDict[int, int] = defaultdict(lambda: 0) for _, digits in lines: for digit in digits: lengths[len(digit)] += 1 # 1, 4, 7, and 8 use a unique number of digits. print(f"1: {lengths[2]}") print(f"4: {lengths[4]}") print(f"7: {lengths[3]}") print(f"8: {lengths[7]}") print(f"Sum: {sum(map(lengths.get, (2, 4, 3, 7)))}") def part2(lines: Sequence[Tuple[Sequence[str], Sequence[str]]]): # This is a table of 0-9 on the 7-seg display based on which lines are used: # # We can use this to figure out digits: # 1: EASY - Has 2 segments # 4: EASY - Has 4 segments # 7: EASY - Has 3 segments # 8: EASY - Has 7 segments # 2: Uniquely shares the C segment with 1 # 9: Shares all segments with 4, and also has two more # 3: Subtract 2 from 8, and subtract that result from 9 # 6: Subtract C segment from 8 # 5: Subtract C and E segments from 8 # 0: Last remaining that we haven't figured out yet, no tricks here nums = [] for patterns_, digits in lines: # using the name patterns_ so re-assignment isn't an issue thanks to mypy :| patterns = [frozenset(pat) for pat in patterns_] mappings = {} rmappings = {} for pat in patterns: if len(pat) == 2: mappings[pat] = 1 rmappings[1] = pat elif len(pat) == 4: mappings[pat] = 4 rmappings[4] = pat elif len(pat) == 3: mappings[pat] = 7 rmappings[7] = pat elif len(pat) == 7: mappings[pat] = 8 rmappings[8] = pat # Figure out 2: # There is one segment that is shared between 1 and 2, and it's the *only* # segment where that is the case. shared: DefaultDict[str, List[FrozenSet[str]]] = defaultdict(list) for pat in patterns: matches = rmappings[1] & pat shared[setstr(matches)] += [pat] # Whichever segment in shared has a length of 1 is seg value 2 for match, pats in shared.items(): if len(pats) == 1: pat = pats[0] mappings[pat] = 2 rmappings[2] = pat break # pprint.pprint(shared) # Figure out 9: for pat in patterns: matches = rmappings[4] & pat # If the matching segments are shared with 4's segments, and there # are 2 more segments, then it's 9 if matches == rmappings[4] and len(pat) == len(rmappings[4]) + 2: mappings[pat] = 9 rmappings[9] = pat break # Figure out 3: # Subtract the segments of 2 and 1 from the 8 seg, and then subtract # *that* from the 9 seg b_seg = (rmappings[8] - rmappings[2]) - rmappings[1] three = rmappings[9] - b_seg assert three in patterns assert three not in mappings mappings[three] = 3 rmappings[3] = three # Few more segments a_seg = rmappings[7] - rmappings[1] assert len(a_seg) == 1 c_seg = rmappings[2] & rmappings[1] assert len(c_seg) == 1 e_seg = rmappings[8] - rmappings[9] assert len(e_seg) == 1 f_seg = rmappings[1] - c_seg assert len(f_seg) == 1 # Figure out 6: # Subtract c segment from 8 six = rmappings[8] - c_seg assert six in patterns assert six not in mappings mappings[six] = 6 rmappings[6] = six # Figure out 5: # Subtract e and c segments from 8 five = (rmappings[8] - e_seg) - c_seg assert five in patterns assert five not in mappings mappings[five] = 5 rmappings[5] = five # There's only one number we haven't mapped yet and that's 0, so that's # the last one. remaining = frozenset(patterns) - frozenset(mappings.keys()) assert len(remaining) == 1 zero = list(remaining)[0] mappings[zero] = 0 rmappings[0] = zero assert len(mappings) == 10 assert len(rmappings) == 10 # Now, figure out each number using our mappings num = 0 for digit in digits: num *= 10 num += mappings[frozenset(digit)] nums += [num] print(sum(nums)) lines = [ # yeah the line.split[0] and [1] is annoying but it makes it work for a # comprehension. also it doesn't matter (line.split(" | ")[0].split(), line.split(" | ")[1].split()) for line in sys.stdin ] # pprint.pprint(lines) print("Part 1") part1(lines) print("Part 2") part2(lines)