One of the recent Advent of code challenges tasks me with solving for the smallest amount of input material that I can use to apply a given set of reactions and get 1 unit of output material.
For example, given
10 ORE => 10 A
1 ORE => 1 B
7 A, 1 B => 1 C
7 A, 1 C => 1 D
7 A, 1 D => 1 E
7 A, 1 E => 1 FUEL
We need 31 total ore to make 1 fuel (1 to produce a unit of B, then 30 to make the requisite 28 A).
This year, I've been trying to push my programming-language horizons, so I've done most of the challenges in SML/NJ. This one seems—seemed—like a good fit for Prolog, given the little I know about it: logic programming, constraint solving, etc.
I was able to put together a working version of this, with some help, and I found the time to read a few tutorials on CHR programming. I think I mostly understand that, below, we say that if we know 10 a(1)
items (what are these called? proofs?), then we can replace that with ore(10)
—which will be expanded into 10 separate ore(1)
calls.
%prolog
:- module(ore, [ fuel/1 ]).
:- use_module(library(chr)).
:- use_module(library(clpfd)).
% constraint names
% ore and fuel are guaranteed
:- chr_constraint
ore/1,
a/1,
b/1,
c/1,
d/1,
e/1,
fuel/1.
% problem constraints
a(1),a(1),a(1),a(1),a(1),a(1),a(1),a(1),a(1),a(1) <=> ore(10).
b(1) <=> ore(1).
c(1) <=> a(7), b(1).
d(1) <=> a(7), c(1).
e(1) <=> a(7), d(1).
fuel(1) <=> a(7), e(1).
% decompositions (one per element???)
a(X) <=> X #> 1, Y #= X-1 | a(Y), a(1).
b(X) <=> X #> 1, Y #= X-1 | b(Y), b(1).
c(X) <=> X #> 1, Y #= X-1 | a(Y), a(1).
d(X) <=> X #> 1, Y #= X-1 | d(Y), d(1).
e(X) <=> X #> 1, Y #= X-1 | e(Y), e(1).
ore(X) <=> X#> 1, Y #= X-1 | ore(Y), ore(1).
% aggregation (for convenience)
% always present
:- chr_constraint ore_add/1, total_ore/1.
total_ore(A), total_ore(Total) <=> NewTotal #= A + Total, total_ore(NewTotal).
ore(1) <=> total_ore(1).
It would be really nice to be able to write the following, though:
a(10) <=> ore(10)
and somehow tell prolog that if I "know," e.g., a(8)
, that I can still replace that ore(10)
(since I need 10 ore to make those 8 a, and some are just wasted).
I think that if I can accomplish this, I can turn this output
?- fuel(1).
ore:a(1),
ore:a(1),
ore:a(1),
ore:a(1),
ore:a(1),
ore:a(1),
ore:a(1),
ore:a(1),
ore:total_ore(21).
into
?- fuel(1).
ore:total_ore(31).
If I manually add ,ore:a(2)
to the fuel query, I get the correct total ore.
To sum up,
- How can I express the constraint that I cannot partially run a reaction, but I can run the reaction to get more than I need? In other words, if I need
a(8)
, that's enough to know I needa(10)
? This, I think, will also allow me to write the original problem statement in language likea(10) <=> ore(10)
, instead of the absurda(1),a(1)...
. Or will it? - Will this solve my "searching" problem, such that
fuel(1)
will give meore:total_ore(31)
?
Update: I spent some time playing with (1) to get
%prolog
:- module(ore, [ fuel/1 ]).
:- use_module(library(chr)).
:- use_module(library(clpfd)).
% constraint names
% ore and fuel are guaranteed
:- chr_constraint
ore/1,
a/1,
b/1,
c/1,
d/1,
e/1,
fuel/1.
% problem constraints
a(X) <=> X #>= 1, X #=< 10 | ore(10).
b(1) <=> ore(1).
c(1) <=> a(7), b(1).
d(1) <=> a(7), c(1).
e(1) <=> a(7), d(1).
fuel(1) <=> a(7), e(1).
% decompositions (one per element???)
b(X) <=> X #> 1, Y #= X-1 | b(Y), b(1).
c(X) <=> X #> 1, Y #= X-1 | a(Y), a(1).
d(X) <=> X #> 1, Y #= X-1 | d(Y), d(1).
e(X) <=> X #> 1, Y #= X-1 | e(Y), e(1).
ore(X) <=> X#> 1, Y #= X-1 | ore(Y), ore(1).
% aggregation (for convenience)
% always present
:- chr_constraint ore_add/1, total_ore/1.
total_ore(A), total_ore(Total) <=> NewTotal #= A + Total, total_ore(NewTotal).
ore(1) <=> total_ore(1).
Which produced total_ore(41)
for my query. I believe the "leftovers" are getting converted to ore, which in this case is not what I want (though it is of course specified). The a
s are now being removed too early—i.e., this is correct but not minimal. How to prefer whole reactions? Hm...