|
1 | 1 | """Codyssi Day N.""" |
2 | 2 |
|
| 3 | +import dataclasses |
3 | 4 | import functools |
4 | | -import logging |
5 | 5 |
|
6 | | -log = logging.info |
| 6 | + |
| 7 | +@dataclasses.dataclass(slots=True, order=True) |
| 8 | +class Recipe: |
| 9 | + """Dataclass to hold recipe info.""" |
| 10 | + quality: int |
| 11 | + cost: int |
| 12 | + materials: int |
| 13 | + |
7 | 14 |
|
8 | 15 | def solve(part: int, data: str, testing: bool) -> int: |
9 | 16 | """Solve the parts.""" |
| 17 | + # Parse the recipes. Store them in a list. We can reference them by index. |
10 | 18 | lines = data.splitlines() |
11 | | - items = [] |
12 | | - byname = {} |
| 19 | + recipes = [] |
13 | 20 | for line in lines: |
14 | 21 | for p in ["| Quality :", ", Cost :", ", Unique Materials :"]: |
15 | 22 | line = line.replace(p, "") |
16 | | - num, name, quality, cost, materials = line.split() |
17 | | - items.append((int(quality), int(cost), name, int(materials))) |
18 | | - byname[name] = (int(cost), int(quality), int(materials)) |
| 23 | + _, _, quality, cost, materials = line.split() |
| 24 | + recipes.append(Recipe(int(quality), int(cost), int(materials))) |
19 | 25 |
|
20 | | - items.sort(reverse=True) |
| 26 | + # Sort, highest quality first. |
| 27 | + recipes.sort(reverse=True) |
21 | 28 |
|
22 | 29 | @functools.cache |
23 | | - def optimal_production(items, budget): |
24 | | - available = sorted((i for i in items if byname[i][0] <= budget), key=lambda i: byname[i][0]) |
25 | | - if not available: |
| 30 | + def optimal_production(items: tuple[int, ...], budget: int) -> tuple[int, int]: |
| 31 | + """Return the optimal production for a given set of items and a given budget. |
| 32 | +
|
| 33 | + Items are simply refered to by their recipe index. |
| 34 | + """ |
| 35 | + # Base case. No items in the budget. Nothing to produce. |
| 36 | + if not items: |
26 | 37 | return 0, 0 |
27 | | - one, *rest = available |
28 | | - f_rest = frozenset(rest) |
29 | | - q_with, m_with = optimal_production(f_rest, budget - byname[one][0]) |
30 | | - q_with += byname[one][1] |
31 | | - m_with += byname[one][2] |
32 | | - q_wout, m_wout = optimal_production(f_rest, budget) |
| 38 | + # Select the first (highest quality) item. Compare the optimal production we can achieve assuming |
| 39 | + # (1) we do produce this item vs (2) we do not. In either case, we explore the optimal production |
| 40 | + # of the remaining items, removing this item from the list. |
| 41 | + one = recipes[items[0]] |
| 42 | + # Case with this one item being produced. Update budget and remaining options. |
| 43 | + budget_with = budget - one.cost |
| 44 | + rest_with = tuple(i for i in items[1:] if recipes[i].cost <= budget_with) |
| 45 | + q_with, m_with = optimal_production(rest_with, budget_with) |
| 46 | + # Add the quality and materials of this one item. |
| 47 | + q_with += one.quality |
| 48 | + m_with += one.materials |
| 49 | + # Case without this one item. Budget, quality, materials do not change. |
| 50 | + # Remaining options only remove one item. |
| 51 | + q_wout, m_wout = optimal_production(items[1:], budget) |
| 52 | + # Pick the higher quality result. |
33 | 53 | if q_wout > q_with: |
34 | 54 | return q_wout, m_wout |
35 | 55 | if q_with > q_wout: |
36 | 56 | return q_with, m_with |
| 57 | + # On a tie, take the lower materials. |
37 | 58 | return q_with, min(m_with, m_wout) |
38 | 59 |
|
39 | 60 | if part == 1: |
40 | | - return sum(i[-1] for i in items[:5]) |
| 61 | + return sum(i.materials for i in recipes[:5]) |
41 | 62 | if part == 2: |
42 | 63 | budget = 30 |
43 | 64 | elif testing: |
44 | 65 | budget = 150 |
45 | 66 | else: |
46 | 67 | budget = 300 |
47 | | - q, m = optimal_production(frozenset(byname), budget) |
| 68 | + q, m = optimal_production(tuple(range(len(recipes))), budget) |
48 | 69 | return q * m |
49 | 70 |
|
50 | 71 |
|
|
0 commit comments