diff --git a/2015/d15/ex2/ex2.py b/2015/d15/ex2/ex2.py new file mode 100755 index 0000000..1861296 --- /dev/null +++ b/2015/d15/ex2/ex2.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +import sys +from collections.abc import Iterator +from typing import Literal, NamedTuple, cast + + +class Properties(NamedTuple): + capacity: int + durability: int + flavor: int + texture: int + calories: int + + @classmethod + def from_str(cls, input: str) -> "Properties": + properties = map(str.split, input.split(", ")) + return cls(*(int(prop[-1]) for prop in properties)) + + +PropertyName = Literal["capacity", "durability", "flavor", "texture", "calories"] + + +def solve(input: str) -> int: + def parse_line(input: str) -> tuple[str, Properties]: + ingredient, properties = input.split(": ") + return ingredient, Properties.from_str(properties) + + def parse(input: str) -> dict[str, Properties]: + return {name: prop for name, prop in map(parse_line, input.splitlines())} + + def sum_properties( + ingredients: dict[str, Properties], + amounts: dict[str, int], + prop: PropertyName, + ) -> int: + return sum( + getattr(ingredients[name], prop) * amounts[name] + for name in ingredients.keys() + ) + + def score(ingredients: dict[str, Properties], amounts: dict[str, int]) -> int: + assert ingredients.keys() == amounts.keys() # Sanity check + assert sum(amounts.values()) == 100 # Sanity check + res = 1 + for prop in ("capacity", "durability", "flavor", "texture"): + res *= max( + 0, + sum_properties(ingredients, amounts, cast(PropertyName, prop)), + ) + return res + + def permute_amounts(ingredients: dict[str, Properties]) -> Iterator[dict[str, int]]: + def helper(amounts: dict[str, int]) -> Iterator[dict[str, int]]: + remaining = 100 - sum(amounts.values()) + assert remaining >= 0 # Sanity check + assert ingredients # Sanity check + + current = next(iter(n for n in ingredients.keys() if n not in amounts)) + if (len(amounts) + 1) == len(ingredients): + yield amounts | {current: remaining} + else: + for i in range(remaining): + yield from helper(amounts | {current: i}) + + yield from helper({}) + + def maximize_score(ingredient: dict[str, Properties]) -> int: + return max( + score(ingredient, amounts) + for amounts in permute_amounts(ingredients) + if sum_properties(ingredient, amounts, "calories") == 500 + ) + + ingredients = parse(input) + return maximize_score(ingredients) + + +def main() -> None: + input = sys.stdin.read() + print(solve(input)) + + +if __name__ == "__main__": + main()