8
votes
Day 14: Space Stoichiometry
Today's problem description: https://adventofcode.com/2019/day/14
Join the Tildes private leaderboard! You can do that on this page, by entering join code 730956-de85ce0c
.
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python
with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Funky questions like these are always amazingly fun or absolutely terrifying. This one is more than a little of both.
Part 1 Strategy
Before we get to anything else, we need to make sure we know what exactly we're solving.It's quite clearly graph related, and we pick out that each chemical (except the start) is produced by exactly one reaction (this will be important later on) and that we're trying to get to an end chemical (FUEL).
The graph you use is up to you, since this one doesn't explicitly define a particular graph, but our graph construction will work as follows:
In the graph each vertex represents a chemical and has weighted edges from a chemical to all the ones used to make it, where A -> B with weight W means that A needs W of B to make it (We're exploiting the fact that every A can only be made 1 way here)
We're using this graph representation so that we can "propagate out" the material needs from the 1 FUEL to each of its dependencies, eventually converging on ORE, which has no dependencies.
In essence, if we know that we need at least N of A and that the formula that makes A makes M amount of it but takes P and Q amounts of B and C, we then know that we will need to make
ceil(N/M)
batches of A, which addsceil(N/M)*P
andceil(N/M)*Q
additional amounts of B and C when accounting for A.Doing this for all the materials in the right order will get us back to ORE, and that big number will be our final answer.
Of course, "right order" isn't trivial here, since it isn't obvious to know when you have all of a material accounted for and when you can say to "propagate out" its dependencies.
However, assuming our input doesn't have a dependency hell hidden in it (which it shouldn't), this graph has no cycles, and for acyclic graphs other smart people have thought about it before.
Using a Topological Sort, we can ensure that every node is completely accounted for when we propagate out its dependencies (the below code uses Kahn's algorithm but any one of these will do), and, running this through until we reach ORE, we can just output how much of ORE is needed according to the graph.
From the top:
ceil(N/M)*W
for N product needed, M made by recipe, and W needed ingredient per recipe)Aside for input parsing: I know that at least in C++, string parsing is pretty bad, so you'll have to fiddle with that too. My (mildly gross) approach is prefaced in the code.
Part 1 Code
Part 2 Strategy
This one isn't actually 1 trillion / (Our Part 1 Answer), as much as we want it to be.This is because we can reuse leftover materials from making the first FUEL unit to make the next with less materials, but this issue isn't hard to solve.
Note that from our part 1 construction we simply set 1 to our initial FUEL count, then ran a graph thing to get the minimum number of ORE to make 1 FUEL.
So instead of setting 1 to the initial count, we can set it to N for any N, run the graph thing, and get the minimum number of ORE to make N FUEL.
We definitely need to do this multiple times, so we have to set up stuff to store the initial graph conditions, but other than that, we can just test values for N to find out the biggest one that takes <= 10^12 ORE.
Of course, to do this in a structured way we can use binary search (yay algorithms!) where a FUEL amount that needs at most 10^12 ORE is a "success" (increase the lower bound) and one that needs more than 10^12 is a "failure" (decrease the upper bound), where you can return the last "success".
Part 2 Code
This one took me the longest so far (other than Day 1, which I didn't start until very late), but I still finished at around the same rank that I have been which makes me feel a tad better (1594/1431).
I decided to start with the Ruby implementation first, since I knew I'd want to poke at it from the repl a bunch during testing. Unfortunately, I'd got used to Crystals pass-by-value stack-allocated struct behaviors, and Ruby likes to pass things by references. So when I parsed out the input into Rule and Value objects, the algorithm would do what I expected once, but alter the rules it was using during the process and fail every time after that. Tracking this down took me much longer than it should've, but I did get there in the end (
freeze
helped track down where the inappropriate writes were happening).Part 1 Spoilers
Parsing the input was straight forward, I chose to use
Value
(tuple of name and amount) andRule
objects, which was easy but as I mentioned above I think it caused more trouble than it was worth. Keeping everything as simpler numbers where possible would helped avoid the mutating objects issue I ran into.Once I had the rules parsed out into a map, we know how much of each resource it takes to produce any given resource, and to get the total ore we can walk the algorithm backwards.
There are three important functions:
reqs_for_value
takes in aValue
(resource "R" and amount "N") and returns a list of values required to produce at least N units of R, and if the rules used result in any excess R floating around, we also return the amount of extra R produced in the reaction. So for the first sample problem, if I need to get the requirements for14 A
, then the function returns[20 ORE], 6
.collapse_values
is more of a utility function to take a list of values and merge any values with the same resource into one. So if we have10 A, 3 ORE, 3 A
it will return13 A, 3 ORE
. I think switching to a hashmap might've allowed me to skip this step. This is used to help keep our working set tidy while we're running.breakdown
is the actual solution to Part 1, this walks the ore production process backwards, deducing how much of each resource were produced and consumed along the way. We have our working setvalues
, and we loop until either we run out of values in the set or the only resource left is ORE.It works like this:
needed
out of the working set (1 FUEL to begin)reqs
and the amount of extra that will be producedneeded
+ the amount ofextra
produced during the reaction.reqs
to the working setPart 1 Ruby
Part 2
Once Part 1 is working correctly, it's relatively easy (still took me a while to fix an off-by-one and port to Crystal) to write a binary search to find the biggest amount of fuel that can be produced without going over an ORE requirement of one trillion.
Weirdly, the first pass of my Crystal port gets the correct answer for part 2, but outputs a slightly wrong value for part 1, despite the logic being more or less identical. Not sure where it's going wrong yet, but I can fix it later.
Well I got distracted with the end of my semester and the start of the holiday season but I am trying to catch up if possible. Code below is for both parts in Python, split by file.
Reaction.py
Stoich.py