Advent of Code 2022 day 19
Branching is incredibly expensive. Theoretically, this problem wasn’t particularly challenging. In practice though, it was infuriating. Many different attempts to speed up the program have proved to be unfruitful. The answers were obtained from half-mangled programs which have been lost to the ether. Below are the remnants.
Part 1
from itertools import product from functools import cached_property class Blueprint: def __init__( self, number, ore_robot_ore, clay_robot_ore, obsidian_robot_ore, obsidian_robot_clay, geode_robot_ore, geode_robot_obsidian, ): self.number = number self.ore_robot_ore = ore_robot_ore self.clay_robot_ore = clay_robot_ore self.obsidian_robot_ore = obsidian_robot_ore self.obsidian_robot_clay = obsidian_robot_clay self.geode_robot_ore = geode_robot_ore self.geode_robot_obsidian = geode_robot_obsidian def __hash__(self): return self.number def get_blueprints(line): ( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian ) = [int(x) for x in line.replace(":", "").split(" ") if x.isnumeric()] return Blueprint( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian, ) with open("19.txt") as f: # with open("19testtest.txt") as f: lines = f.read().splitlines() max_geode = 0 history = {} def tree( blueprint, time = 24, ore = 0, clay = 0, obsidian = 0, geode = 0, ore_robot = 1, clay_robot = 0, obsidian_robot = 0, geode_robot = 0, add_clay_robot = True, add_ore_robot = True, ): global max_geode global history key = ( time, ore, clay, obsidian, geode, ore_robot, clay_robot, geode_robot, add_clay_robot and blueprint.clay_robot_ore <= ore, add_ore_robot and blueprint.ore_robot_ore <= ore, ) if key in history: return history[key] # print( # "blueprint:", # blueprint.number, # "time:", # time, # "max_geode:", # max_geode, # "geode:", # geode, # "robots:", # geode_robot, # obsidian_robot, # clay_robot, # ore_robot, # "ores:", # ore, # clay, # obsidian, # geode, # ) if geode > max_geode: max_geode = geode if time == 0: history[key] = geode return geode if (geode + (1.5*time*(geode_robot+1)) < max_geode): history[key] = 0 return 0 new_geode_robot = geode_robot new_obsidian_robot = obsidian_robot new_clay_robot = clay_robot new_ore_robot = ore_robot if ( blueprint.geode_robot_ore <= ore and blueprint.geode_robot_obsidian <= obsidian ): ore -= blueprint.geode_robot_ore obsidian -= blueprint.geode_robot_obsidian new_geode_robot += 1 elif ( blueprint.obsidian_robot_ore <= ore and blueprint.obsidian_robot_clay <= clay ): ore -= blueprint.obsidian_robot_ore clay -= blueprint.obsidian_robot_clay new_obsidian_robot += 1 elif ( add_clay_robot and blueprint.clay_robot_ore <= ore ): ore -= blueprint.clay_robot_ore new_clay_robot += 1 elif ( add_ore_robot and blueprint.ore_robot_ore <= ore ): ore -= blueprint.ore_robot_ore new_ore_robot += 1 ans = max( tree( blueprint, time = time-1, ore = ore + ore_robot, clay = clay + clay_robot, obsidian = obsidian + obsidian_robot, geode = geode + geode_robot, ore_robot = new_ore_robot, clay_robot = new_clay_robot, obsidian_robot = new_obsidian_robot, geode_robot = new_geode_robot, add_clay_robot = new_add_clay_robot, add_ore_robot = new_add_ore_robot, ) for ( new_add_clay_robot, new_add_ore_robot ) in product((True, False), repeat=2) ) history[key] = ans return ans def get_geodes(blueprint): global max_geode global history max_geode = 0 history = {} ans = tree(blueprint) print(blueprint.number, ans, max_geode) return tree(blueprint) blueprints = [get_blueprints(line) for line in lines] print( "answer", sum([ get_geodes(blueprint) * blueprint.number for blueprint in blueprints ]) )
Part 2 (incredibly slow)
from itertools import product from random import randint from functools import cached_property class Blueprint: def __init__( self, number, ore_robot_ore, clay_robot_ore, obsidian_robot_ore, obsidian_robot_clay, geode_robot_ore, geode_robot_obsidian, ): self.number = number self.ore_robot_ore = ore_robot_ore self.clay_robot_ore = clay_robot_ore self.obsidian_robot_ore = obsidian_robot_ore self.obsidian_robot_clay = obsidian_robot_clay self.geode_robot_ore = geode_robot_ore self.geode_robot_obsidian = geode_robot_obsidian def __hash__(self): return self.number def __str__(self): return ( f"{self.number} " f"ore ore: {self.ore_robot_ore} " f"clay ore:{self.clay_robot_ore} " f"obsidian ore: {self.obsidian_robot_ore} " f"obsidian clay: {self.obsidian_robot_clay} " f"geode ore: {self.geode_robot_ore} " f"geode obsidian: {self.geode_robot_obsidian} " ) def get_blueprints(line): ( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian ) = [int(x) for x in line.replace(":", "").split(" ") if x.isnumeric()] return Blueprint( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian, ) with open("19.txt") as f: lines = f.read().splitlines() def tree( blueprint, time = 24, ore = 0, clay = 0, obsidian = 0, geode = 0, ore_robot = 1, clay_robot = 0, obsidian_robot = 0, geode_robot = 0, ): def can_build(t=None): if t is None: return [ x for x in ["", "geode", "obsidian", "clay", "ore"] if can_build(x) ] elif t == "": return True elif t == "geode": return ( blueprint.geode_robot_ore <= ore and blueprint.geode_robot_obsidian <= obsidian ) elif t == "obsidian": return ( blueprint.obsidian_robot_ore <= ore and blueprint.obsidian_robot_clay <= clay ) elif t == "clay": return blueprint.clay_robot_ore <= ore elif t == "ore": return blueprint.ore_robot_ore <= ore def new_ore(build): if build == "": return ore + ore_robot elif build == "ore": return ore + ore_robot - blueprint.ore_robot_ore elif build == "clay": return ore + ore_robot - blueprint.clay_robot_ore elif build == "obsidian": return ore + ore_robot - blueprint.obsidian_robot_ore elif build == "geode": return ore + ore_robot - blueprint.geode_robot_ore def new_clay(build): if build == "obsidian": return clay + clay_robot - blueprint.obsidian_robot_clay return clay + clay_robot def new_obsidian(build): if build == "geode": return obsidian + obsidian_robot - blueprint.geode_robot_obsidian return obsidian + obsidian_robot def new_geode(build): return geode + geode_robot def new_ore_robot(build): if build == "ore": return ore_robot + 1 return ore_robot def new_clay_robot(build): if build == "clay": return clay_robot + 1 return clay_robot def new_obsidian_robot(build): if build == "obsidian": return obsidian_robot + 1 return obsidian_robot def new_geode_robot(build): if build == "geode": return geode_robot + 1 return geode_robot def get_most_needed(t=None, weights=None): values = {} if t is None: t = ["geode", "obsidian", "clay", "ore", ""] if weights is None: weights = [10**6, 20, 10, 6, 3, 2, 1, 30, 1] for x in t: if x == "geode": values[x] = weights[0] elif x == "obsidian": values[x] = ( ( blueprint.geode_robot_obsidian - obsidian_robot )*weights[1] ) elif x == "clay": values[x] = ( (blueprint.geode_robot_obsidian - obsidian_robot)*weights[2] + (blueprint.obsidian_robot_clay - clay_robot)*weights[3] ) elif x == "ore": values[x] = ( (blueprint.geode_robot_ore - ore_robot)*weights[4] + (blueprint.obsidian_robot_ore - ore_robot)*weights[5] + (blueprint.clay_robot_ore - clay_robot)*weights[6] + (blueprint.ore_robot_ore - ore_robot)*weights[7] ) elif x == "": values[x] = weights[8] return sorted(values, key=lambda x: values[x], reverse=True) def should_build(): return get_most_needed(can_build()) key = ( time, "ores", ore, clay, obsidian, geode, "robots", ore_robot, clay_robot, obsidian_robot, geode_robot, ) global max_geodes if geode > max_geodes: max_geodes = geode print(max_geodes) if time == 0: print(key) return geode if geode + time*(geode_robot + time/2) < max_geodes: return 0 return max( tree( blueprint, time = time-1, ore = new_ore(build), clay = new_clay(build), obsidian = new_obsidian(build), geode = new_geode(build), ore_robot = new_ore_robot(build), clay_robot = new_clay_robot(build), obsidian_robot = new_obsidian_robot(build), geode_robot = new_geode_robot(build), ) for build in should_build() ) # for build in can_build(): # if build == "geode": # ore -= blueprint.geode_robot_ore # obsidian -= blueprint.geode_robot_obsidian # new_geode_robot += 1 # elif build == "obsidian": # ore -= blueprint.obsidian_robot_ore # clay -= blueprint.obsidian_robot_clay # new_obsidian_robot += 1 # elif build == "clay": # ore -= blueprint.clay_robot_ore # new_clay_robot += 1 # elif build == "ore": # ore -= blueprint.ore_robot_ore # new_ore_robot += 1 # return tree( # blueprint, # time = time-1, # ore = ore + ore_robot, # clay = clay + clay_robot, # obsidian = obsidian + obsidian_robot, # geode = geode + geode_robot, # ore_robot = new_ore_robot, # clay_robot = new_clay_robot, # obsidian_robot = new_obsidian_robot, # geode_robot = new_geode_robot, # ) max_geodes = 0 def get_geodes(blueprint, time=24): global max_geodes max_geodes = 0 print(blueprint) ans = tree(blueprint, time=time) print(blueprint.number, ans) return ans blueprints = [get_blueprints(line) for line in lines] answer = 1 for blueprint in blueprints[:3]: geodes = get_geodes(blueprint, time=32) answer *= geodes print(answer)
Part 2 (quick, but not necessarily accurate)
from itertools import product from random import randint from functools import cached_property class Blueprint: def __init__( self, number, ore_robot_ore, clay_robot_ore, obsidian_robot_ore, obsidian_robot_clay, geode_robot_ore, geode_robot_obsidian, ): self.number = number self.ore_robot_ore = ore_robot_ore self.clay_robot_ore = clay_robot_ore self.obsidian_robot_ore = obsidian_robot_ore self.obsidian_robot_clay = obsidian_robot_clay self.geode_robot_ore = geode_robot_ore self.geode_robot_obsidian = geode_robot_obsidian def __hash__(self): return self.number def __str__(self): return ( f"{self.number} " f"ore ore: {self.ore_robot_ore} " f"clay ore:{self.clay_robot_ore} " f"obsidian ore: {self.obsidian_robot_ore} " f"obsidian clay: {self.obsidian_robot_clay} " f"geode ore: {self.geode_robot_ore} " f"geode obsidian: {self.geode_robot_obsidian} " ) def get_blueprints(line): ( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian ) = [int(x) for x in line.replace(":", "").split(" ") if x.isnumeric()] return Blueprint( number, ore_robot, clay_robot, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian, ) with open("19.txt") as f: lines = f.read().splitlines() def generate_weights(n=10000): for _ in range(n): yield [ 10**6, randint(1, 25), randint(1, 25), randint(1, 25), randint(1, 25), randint(1, 25), randint(1, 25), randint(1, 25), randint(1, 25), ] # for a, b, c, d, e, f, g in product( # (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20), # repeat=7 # ): # yield [10**9, a, b, c, d, e, f, g] def tree( blueprint, time = 24, ore = 0, clay = 0, obsidian = 0, geode = 0, ore_robot = 1, clay_robot = 0, obsidian_robot = 0, geode_robot = 0, weights = None, ): def can_build(t=None): if t is None: return [ x for x in ["geode", "obsidian", "clay", "ore"] if can_build(x) ] elif t == "geode": return ( blueprint.geode_robot_ore <= ore and blueprint.geode_robot_obsidian <= obsidian ) elif t == "obsidian": return ( blueprint.obsidian_robot_ore <= ore and blueprint.obsidian_robot_clay <= clay ) elif t == "clay": return blueprint.clay_robot_ore <= ore elif t == "ore": return blueprint.ore_robot_ore <= ore # def time_to_build(t): # if t == "obsidian": # return max( # 0, # (blueprint.obsidian_robot_ore - ore)/ore_robot, # (blueprint.obsidian_robot_clay - clay)/clay_robot # ) # elif t == "clay": # return max( # 0, # int((blueprint.clay_robot_ore - ore)/ore_robot) # ) # elif t == "ore": # return max( # 0, # int((blueprint.ore_robot_ore - ore)/ore_robot) # ) def get_most_needed(t=None, weights=None): values = {} if t is None: t = ["geode", "obsidian", "clay", "ore"] if weights is None: weights = [10**6, 10, 5, 2, 5, 2, 1, 30] for x in t: if x == "geode": values[x] = weights[0] elif x == "obsidian": values[x] = ( ( blueprint.geode_robot_obsidian - obsidian_robot )*weights[1] ) elif x == "clay": values[x] = ( (blueprint.geode_robot_obsidian - obsidian_robot)*weights[2] + (blueprint.obsidian_robot_clay - clay_robot)*weights[3] ) elif x == "ore": values[x] = ( (blueprint.geode_robot_ore - ore_robot)*weights[4] + (blueprint.obsidian_robot_ore - ore_robot)*weights[5] + (blueprint.clay_robot_ore - clay_robot)*weights[6] + (blueprint.ore_robot_ore - ore_robot)*weights[7] ) if len(values): return sorted(values, key=lambda x: values[x])[-1] return "" def should_build(weights): return get_most_needed(can_build(), weights=weights) # def robot_worth_it(t): # if t == "geode": # return can_build(t) # elif t == "obsidian": # return ( # can_build(t) and # not ( # ( # obsidian + obsidian_robot >= # blueprint.geode_robot_obsidian # ) and # blueprint.obsidian_robot_ore < blueprint.geode_robot_ore # ) # ) # elif t == "clay": # return ( # can_build(t) and # not ( # ( # obsidian + obsidian_robot >= # blueprint.geode_robot_obsidian # ) and # blueprint.clay_robot_ore < blueprint.geode_robot_ore # ) and # not ( # ore <= blueprint.obsidian_robot_ore and # blueprint.clay_robot_ore < blueprint.obsidian_robot_ore # ) # ) # elif t == "ore": # return ( # can_build(t) and # blueprint.ore_robot_ore <= ore and # not ( # ore < blueprint.clay_robot_ore and # blueprint.ore_robot_ore < blueprint.clay_robot_ore # ) # ) key = ( time, "ores", ore, clay, obsidian, geode, "robots", ore_robot, clay_robot, obsidian_robot, geode_robot, ) if time == 0: # print(key) return geode new_geode_robot = geode_robot new_obsidian_robot = obsidian_robot new_clay_robot = clay_robot new_ore_robot = ore_robot build = should_build(weights=weights) if build == "geode": ore -= blueprint.geode_robot_ore obsidian -= blueprint.geode_robot_obsidian new_geode_robot += 1 elif build == "obsidian": ore -= blueprint.obsidian_robot_ore clay -= blueprint.obsidian_robot_clay new_obsidian_robot += 1 elif build == "clay": ore -= blueprint.clay_robot_ore new_clay_robot += 1 elif build == "ore": ore -= blueprint.ore_robot_ore new_ore_robot += 1 return tree( blueprint, time = time-1, ore = ore + ore_robot, clay = clay + clay_robot, obsidian = obsidian + obsidian_robot, geode = geode + geode_robot, ore_robot = new_ore_robot, clay_robot = new_clay_robot, obsidian_robot = new_obsidian_robot, geode_robot = new_geode_robot, weights = weights ) def get_geodes(blueprint, time=24): print(blueprint) ans = max( tree(blueprint, time=time, weights=weights) for weights in generate_weights() ) print(blueprint.number, ans) return ans blueprints = [get_blueprints(line) for line in lines] answer = 1 for blueprint in blueprints[:3]: geodes = get_geodes(blueprint, time=32) answer *= geodes print(answer)