10

I saw this ECLiPSe solution to the problem mentioned in this XKCD comic. I tried to convert this to pure Prolog.

go:-
    Total = 1505,
    Prices = [215, 275, 335, 355, 420, 580],
    length(Prices, N),
    length(Amounts, N),
    totalCost(Prices, Amounts, 0, Total),
    writeln(Total).

totalCost([], [], TotalSoFar, TotalSoFar).
totalCost([P|Prices], [A|Amounts], TotalSoFar, EndTotal):-
    between(0, 10, A),
    Cost is P*A,
    TotalSoFar1 is TotalSoFar + Cost,
    totalCost(Prices, Amounts, TotalSoFar1, EndTotal).

I don't think that this is the best / most declarative solution that one can come up with. Does anyone have any suggestions for improvement? Thanks in advance!

false
  • 10,264
  • 13
  • 101
  • 209
Ashley
  • 829
  • 1
  • 5
  • 16
  • 1
    It seems efficient to work backward from the most expensive items, as these permit smaller Amount multiples to be subtracted from the budget/knapsack (less branching). A simple modification to your existing code would be to check that TotalSoFar is less than or equal to EndTotal. With this in a pencil and paper attempt, I found a solution pretty quickly. – hardmath Jun 06 '11 at 15:10

3 Answers3

6

Since you mention SWI-Prolog why not

?- use_module(library(clpfd)).

and library(lambda)

?- Total = 1505, Prices = [215, 275, 335, 355, 420, 580],
      maplist(\P^A^M^(P*A #= M, A #>=0),Prices,Amounts,Ms),
      sum(Ms, #=, Total).

By stating this, all variables in the list Amounts are in a finite range. So there is no need to "do the math" for an upper bound (which often goes wrong anyway). To see concrete solutions, labeling/2 is needed:

?- Total = 1505, Prices = [215, 275, 335, 355, 420, 580],
      maplist(\P^A^M^(P*A #= M, A #>=0),Prices,Amounts,Ms),
      sum(Ms, #=, Total),
      labeling([], Amounts).
   Total = 1505, Prices = [215,275,335,355,420,580],
   Amounts = [1,0,0,2,0,1], Ms = [215,0,0,710,0,580]
;  Total = 1505, Prices = [215,275,335,355,420,580],
   Amounts = [7,0,0,0,0,0], Ms = [1505,0,0,0,0,0].
false
  • 10,264
  • 13
  • 101
  • 209
5

Your generate-and-test approach should be intelligible to any Prolog programmer with more than a few days experience. Here are some minor tweaks:

go(Amounts) :-
    Prices = [580, 420, 355, 335, 275, 215],
    totalCost(Prices, Amounts, 0, 1505),
    write(Amounts), nl.

totalCost([], [], Total, Total).
totalCost([P|Prices], [A|Amounts], SoFar, Total):-
    Upper is (Total-SoFar)//P,
    between(0,Upper,A),
    SoNear is SoFar + P*A,
    totalCost(Prices, Amounts, SoNear, Total).

I changed go/0 to go/1 so that the Prolog engine will backtrack and produce all the solutions (there are two). The calls to length/2 could be omitted because totalCost/4 does the work of building list Amounts to have equal length with Prices. I used write/1 and nl/0 to make it a little more portable.

In totalCost/4 I shortened some of the variable/argument names and indulged in a slightly jokey name for the accumulator argument. The way I imposed the check that our accumulator doesn't exceed the desired Total uses your original call to between/3 but with a computed upper bound instead of a constant. On my machine it reduced the running time from minutes to seconds.

Added: I should mention here what was said in my comment above, that the menu items are now ordered from most expensive to least. Using SWI-Prolog's time/1 predicate shows this reduces the work from 1,923 inferences to 1,070 inferences. The main improvement (in speed) comes from using computed bounds on A rather than range 0 to 10 for every item.

time((go(A),false)).

Note the extra parentheses around the compound goal, as otherwise SWI-Prolog thinks we are calling an undefined time/2 predicate.

hardmath
  • 8,753
  • 2
  • 37
  • 65
0

Can express simply in clpBNR:

go :-
    Amounts = [A,B,C,D,E,F],
    Amounts::integer(0, _),
    { (A*215) + (B*275) + (C*335) + (D*355) + (E*420) + (F*580) == 1505 },
    solve(Amounts),
    writeln(Amounts).

Result in swi-prolog:

?- time(go).
[1,0,0,2,0,1]
% 35,063 inferences, 0.014 CPU in 0.014 seconds (99% CPU, 2483361 Lips)
true ;
[7,0,0,0,0,0]
% 50,954 inferences, 0.023 CPU in 0.023 seconds (100% CPU, 2262260 Lips)
true.
brebs
  • 3,462
  • 2
  • 3
  • 12