···11+import fileinput
22+from collections import deque
33+44+from utils import mul, parse_nums
55+66+77+# Parse input.
88+BLUEPRINTS = []
99+1010+for line in fileinput.input():
1111+ BLUEPRINTS.append(parse_nums(line))
1212+1313+1414+def simulate(bp, max_time=24):
1515+ id_num, ore_cost, clay_cost, ob_ore, ob_clay, geo_ore, geo_ob = bp
1616+1717+ # Pruning Heuristic #1: Cap Resources
1818+ #
1919+ # Since we can only build one robot per minute, at a certain point,
2020+ # we will have so much of a resource that collecting any more will
2121+ # make no effective difference to the end goal of mining geodes.
2222+ #
2323+ # In order to keep the search space smaller, cap the number of the
2424+ # lower-level resources based on trial-and-error multipliers on the
2525+ # resource costs of the various robots.
2626+ MAX_ORE_MULTIPLIER = 1.5
2727+ MAX_CLAY_MULTIPLER = 1.5
2828+ MAX_OB_MULTIPLIER = 1
2929+3030+ max_ore = (ore_cost + clay_cost + ob_ore + geo_ore) * MAX_ORE_MULTIPLIER
3131+ max_clay = ob_clay * MAX_CLAY_MULTIPLER
3232+ max_ob = geo_ob * MAX_CLAY_MULTIPLER
3333+3434+ # minutes, ore_bots, clay_bots, ob_bots, geo_bots, ore, clay, ob, geo
3535+ state = (1, 1, 0, 0, 0, 0, 0, 0, 0)
3636+ horizon = deque([state])
3737+ seen = {}
3838+3939+ best = 0
4040+4141+ while horizon:
4242+ state = horizon.popleft()
4343+ minutes, ore_bots, clay_bots, ob_bots, geo_bots, ore, clay, ob, geo = state
4444+4545+ # Cap resources before checking/saving state.
4646+ ore = min(ore, max_ore)
4747+ clay = min(clay, max_clay)
4848+ max_ob = min(ob, max_ob)
4949+5050+ key = (minutes, ore_bots, clay_bots, ob_bots, geo_bots)
5151+ val = (ore, clay, ob, geo)
5252+ if key in seen:
5353+ if all(a <= b for a, b in zip(val, seen[key])):
5454+ continue
5555+5656+ if minutes >= max_time + 1:
5757+ if geo > best:
5858+ best = geo
5959+ continue
6060+6161+ seen[key] = val
6262+6363+ next_poss = []
6464+6565+ # Pruning Heuristic #2: Limit Lower Bots
6666+ #
6767+ # Once we get to the "late-game" and are building geode robots,
6868+ # building additional ore or clay (and even obsidian) bots becomes
6969+ # a waste, as it doesn't help improve the ability to build geode
7070+ # robots as quickly as possible.
7171+ #
7272+ # We define some heuristics based on when to stop building the
7373+ # lower-level bots, based on how many geode robots we have.
7474+ GEO_ORE_STOPPAGE = 1
7575+ GEO_CLAY_STOPPAGE = 1
7676+ GEO_OB_STOPPAGE = 7
7777+7878+ # Build a geode bot.
7979+ if ore >= geo_ore and ob >= geo_ob:
8080+ ns = minutes + 1, ore_bots, clay_bots, ob_bots, geo_bots + 1, ore - geo_ore, clay, ob - geo_ob, geo
8181+ next_poss.append(ns)
8282+8383+ # Build an obsidian bot.
8484+ if ore >= ob_ore and clay >= ob_clay and geo_bots < GEO_OB_STOPPAGE:
8585+ ns = minutes + 1, ore_bots, clay_bots, ob_bots + 1, geo_bots, ore - ob_ore, clay - ob_clay, ob, geo
8686+ next_poss.append(ns)
8787+8888+ # Build a clay bot.
8989+ if ore >= clay_cost and geo_bots < GEO_CLAY_STOPPAGE:
9090+ ns = minutes + 1, ore_bots, clay_bots + 1, ob_bots, geo_bots, ore - clay_cost, clay, ob, geo
9191+ next_poss.append(ns)
9292+9393+ # Build an ore bot.
9494+ if ore >= ore_cost and geo_bots < GEO_ORE_STOPPAGE:
9595+ ns = minutes + 1, ore_bots + 1, clay_bots, ob_bots, geo_bots, ore - ore_cost, clay, ob, geo
9696+ next_poss.append(ns)
9797+9898+ # Don't build anything; just accrue resources.
9999+ if ore < max_ore:
100100+ ns = minutes + 1, ore_bots, clay_bots, ob_bots, geo_bots, ore, clay, ob, geo
101101+ next_poss.append(ns)
102102+103103+ # Actually acquire resources.
104104+ for ns in next_poss:
105105+ m, oreb, clayb, obb, geob, ore, clay, ob, geo = ns
106106+ horizon.append((m, oreb, clayb, obb, geob, ore + ore_bots, clay + clay_bots, ob + ob_bots, geo + geo_bots))
107107+108108+ return best
109109+110110+111111+quality_levels = [simulate(bp, max_time=24) * bp[0] for bp in BLUEPRINTS]
112112+print("Part 1:", (sum(quality_levels)))
113113+114114+part_2 = [simulate(bp, max_time=32) for bp in BLUEPRINTS[:3]]
115115+part_2.sort(reverse=True)
116116+print("Part 2:", mul(part_2))