advent-of-code/2020/d21/ex2/ex2.py

82 lines
2.3 KiB
Python
Executable file

#!/usr/bin/env python
import functools
import re
import sys
from collections import defaultdict
from copy import deepcopy
from typing import Dict, List, Set, Tuple
def parse(raw: List[str]) -> Tuple[List[Set[str]], List[Set[str]]]:
def parse_ingredients(line: str) -> Set[str]:
pos = line.find(" (contains ")
if pos != -1:
line = line[:pos]
return set(line.split())
def parse_allergens(line: str) -> Set[str]:
pos = line.find("(contains ")
if pos == -1:
return set()
return set(re.findall("([^ ]+)[,\\)]", line))
ingredients = []
allergens = []
for line in raw:
ingredients.append(parse_ingredients(line))
allergens.append(parse_allergens(line))
return ingredients, allergens
def find_allergens(
ingredients: List[Set[str]], allergens: List[Set[str]]
) -> Dict[str, Set[str]]:
all_ingredients = functools.reduce(lambda lhs, rhs: lhs | rhs, ingredients)
possibilities: Dict[str, Set[str]] = defaultdict(lambda: deepcopy(all_ingredients))
for ing, all in zip(ingredients, allergens):
for allergen in all:
possibilities[allergen] &= set(ing)
return dict(possibilities)
def cross_eliminate(possibilities: Dict[str, Set[str]]) -> None:
while True:
eliminated = False
for pos in possibilities:
if len(possibilities[pos]) != 1:
continue
for other_pos in possibilities:
if other_pos == pos:
continue
if len(possibilities[other_pos] & possibilities[pos]) == 0:
continue
eliminated = True
possibilities[other_pos] -= possibilities[pos]
if not eliminated:
break
def solve(raw: List[str]) -> str:
ingredients, allergens = parse(raw)
possibilities = find_allergens(ingredients, allergens)
cross_eliminate(possibilities)
matches = [
(ingredient.pop(), allergen) for allergen, ingredient in possibilities.items()
]
matches.sort(key=lambda tup: tup[1])
return ",".join(ingredient for ingredient, __ in matches)
def main() -> None:
input = [line.strip() for line in sys.stdin]
print(solve(input))
if __name__ == "__main__":
main()