from dataclasses import dataclass from functools import cache, cached_property from itertools import combinations_with_replacement @dataclass class Ingredient: capacity: int durability: int flavor: int texture: int calories: int @cached_property def score(self): return max(self.capacity, 0) * max(self.durability, 0) * max(self.flavor, 0) * max(self.texture, 0) @cached_property def calories_score(self): return self.score if self.calories == 500 else 0 def __lt__(self, other): return self.score < other.score def __gt__(self, other): return self.score > other.score def __le__(self, other): return self.score <= other.score def __ge__(self, other): return self.score >= other.score def __add__(self, other): return Ingredient( self.capacity + other.capacity, self.durability + other.durability, self.flavor + other.flavor, self.texture + other.texture, self.calories + other.calories ) def __mul__(self, other: int) -> 'Ingredient': return Ingredient( self.capacity * other, self.durability * other, self.flavor * other, self.texture * other, self.calories * other ) @staticmethod def from_string(string) -> 'Ingredient': _, rest = string.split(': ') capacity, durability, flavor, texture, calories = (int(p.split()[1]) for p in rest.split(', ')) return Ingredient(capacity, durability, flavor, texture, calories) # def part1(ingredients: list[Ingredient]) -> int: # @cache # def max_score(capacity: int) -> Properties: # if capacity == 0: # return Properties(0, 0, 0, 0, 0) # return max(max_score(capacity - 1) + ingredient.properties for ingredient in ingredients) # s = max_score(100).score # print(max_score.cache_info()) # return s # def part1(ingredients: list[Ingredient]) -> int: # return max(sum(perm) for perm in combinations_with_replacement([i.properties for i in ingredients], 100)).score def splits(total: int, segments: int) -> int: if segments <= 1: yield [total] else: for i in range(total + 1): for subsplit in splits(total - i, segments - 1): yield [i] + subsplit def part1(ingredients: list[Ingredient]) -> int: empty = Ingredient(0, 0, 0, 0, 0) return max(sum((ingredients[i] * q for i, q in enumerate(split)), start=empty).score for split in splits(100, len(ingredients))) def part2(ingredients: list[Ingredient]) -> int: empty = Ingredient(0, 0, 0, 0, 0) return max(sum((ingredients[i] * q for i, q in enumerate(split)), start=empty).calories_score for split in splits(100, len(ingredients))) if __name__ == '__main__': with open('input') as f: ingredients = [Ingredient.from_string(line.strip()) for line in f] print(part1(ingredients)) print(part2(ingredients))