Ryan is an engineer in the Sacramento Area with a focus in Python, Ruby, and Rust. Bash/Python Exercism mentor. Coding, physics, calculus, music, woodworking. Looking for work!
After much stewing and drawing diagrams, I realized it was an acyclic directed graph. It's always a graph! Part 2 took a little bit of pragmatic, targeted brute forcing because I was sleepy.
"""Make fuel out of ore based on stoichiometric recipes"""fromdataclassesimportdataclassfromtypingimportList,Dictfrommathimportceil@dataclassclassComponent:"""An input component to a reaction"""name:strquantity:int@dataclassclassEquation:"""A stoichiometric reaction equation using multiple ingredients to make some output"""name:strquantity:intingredients:Dict[str,Component]@dataclassclassNode:"""A node in a dependency graph of stoichiometric equations.
Nodes are ingredients. They have dependents (things that use them
as inputs) and dependencies (things they need to make themselves).
Dynamically, they resolve themselves by listening to their dependents
telling them how much of themself they need and, based on the equations,
calculating the minimum amount that can be made to achieve that.
"""name:strdependents:List["Node"]dependencies:List["Node"]amount_needed:int=0amount_made:int=0resolved:bool=Falsedefparse(text):"""Parses input text to build the recipes database.
Expects text in the form of equations (one per line):
3 IMFS, 4 ABCD, 1 EFGH => 6 OUTP
Returns a dict of output names to recipe Equations.
"""result=dict()forlineintext.splitlines():inputs,outputs=line.split(" => ")ingredients=dict()forinpininputs.split(", "):qty,name=inp.split()ingredients[name]=Component(name,int(qty))out_qty,out_name=outputs.split()result[out_name]=Equation(out_name,int(out_qty),ingredients)# Additionally, ORE is free. You can make 1 ORE with no inputs.
result["ORE"]=Equation(1,dict())returnresultdefgenerate_digraph(recipes:Dict[str,Equation])->Dict[str,Node]:"""Generates a graph of component dependencies."""digraph={name:Node(name,[],[])fornameinrecipes}forname,eqninrecipes.items():foringredient_nameineqn.ingredients:digraph[name].dependencies.append(digraph[ingredient_name])digraph[ingredient_name].dependents.append(digraph[name])returndigraphdefresolve_digraph(fuel:int,recipes:Dict[str,Equation],digraph:Dict[str,Node]):"""Based on the desired amount of fuel, calculates how much of each
component is needed, and, based on output amounts in the recipes,
how much of each component is actually created to minimize waste.
Initializes information at FUEL--the top of the graph--and starts
solving recursively from the bottom of the graph: ORE.
"""digraph["FUEL"].amount_needed=fuelresolve(digraph["ORE"],recipes)defresolve(node:Node,recipes:Dict[str,Equation]):"""Resolve a node.
A node can be resolved if all of the components that depend on it
have been resolved and have decided how much of it they need.
Then, it calculates how much should be made based on the recipes.
And it informs its dependencies how much of each of them it will
need.
"""fordependentinnode.dependents:ifnotdependent.resolved:resolve(dependent,recipes)needed=node.amount_neededpossible=recipes[node.name].quantitybatches=ceil(needed/possible)node.amount_made=batches*possiblefordependencyinnode.dependencies:dependency.amount_needed+=batches* \
recipes[node.name].ingredients[dependency.name].quantitynode.resolved=Trueif__name__=="__main__":withopen("data/day14.txt")asf:recipes=parse(f.read())digraph=generate_digraph(recipes)# Part 1: How much ORE to make 1 FUEL?
resolve_digraph(1,recipes,digraph)print(f"Amount of ore needed: {digraph['ORE'].amount_made}")# Part 2: How much FUEL can you make with 1 ORE.
# Actually, I started 'fuel' at 1 and ran for a while to collect
# data points. I graphed it, found the approximate trend line,
# hedged low, and used that new number as my start point.
# It was only a few iterations before I found the real answer :)
fuel=1934973whileTrue:digraph=generate_digraph(recipes)resolve_digraph(fuel,recipes,digraph)ifdigraph["ORE"].amount_made>1_000_000_000_000:print(f"{fuel-1} fuel possible on a trillion ore.")breakprint(f"{fuel} fuel needs {digraph['ORE'].amount_made} ore.")fuel+=1
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
After much stewing and drawing diagrams, I realized it was an acyclic directed graph. It's always a graph! Part 2 took a little bit of pragmatic, targeted brute forcing because I was sleepy.