Variable bin packing problem with Prolog (CLP) - prolog

I am trying to find an algorithm for the NP-hard 2D Variable Size Bin Packing Problem (2DVSBPP) in (Swi-)Prolog using Constraint Logic Programming (CLP).
The problem could be explained like this: some ordered Products need to be packed as efficiently as possible into some Boxes (bins). The products have some given Width and Length (squares or rectangles, eg 2x3). There are four different size of boxes, each with a given cost for the shipper (eg $4 for the 5x5 box, $5 for 5x7 box). The goal is to minimize the total cost from the boxes.
I've been looking for an answer to this problem for a while now and have read numerous papers and similar examples in other languages. However, I can't find any working solution. I'm especially struggling with how to handle the unknown number of Boxes (bins).
To be able to find a solution to this problem I've tried to adapt a similar problem but really have no idea how to handle the variable amount of boxes. The following code can choose the cheapest possible box to fit all the products as long as there is only one box needed to fit them all. From the moment we need multiple boxes, the program just fails.
The boxes and products:
:- use_module(library(clpfd)).
:- use_module(library(clpr)).
:- expects_dialect(sicstus).
%% These are the possible productsizes that could need packing
% product (id, width, length)
product(1, 2, 2).
product(2, 1, 2).
product(2, 2, 1). % repeating product n2 because it can lay horizontal or vertical
product(3, 1, 3).
product(3, 3, 1). % idem
product(4, 3, 3). % is square so does not need it
product(5, 2, 3).
product(5, 3, 2). % iden
product(6, 4, 2).
product(6, 2, 4). % idem
% because it can lay virtically or horizontally in a box
product_either_way(Number, Width, Length) :-
product(Number, Width, Length).
product_either_way(Number, Width, Length) :-
product(Number, Length, Width).
%% These are the so called bins from the 2DVSBPP problem
%% There are 4 sizes, but there is an unlimited supply
% box(Width, Length, Cost)
box(4,4,4).
box(4,6,6).
box(5,5,7).
box(9,9,9).
The constraints:
area_box_pos_combined(W_total*H_total,prod(N),X+Y,f(X,Width,Y,Height)) :-
product_either_way(N, Width, Height), % Getting the width and height (length) of a product
% Constraint: the product should 'fit' inside the choosen box
% thus limiting its coordinates (XY)
X #>= 1,
X #=< W_total-Width+1,
Y #>= 1,
Y #=< H_total-Height+1.
positions_vars([],[]).
positions_vars([X+Y|XYs],[X,Y|Zs]) :-
positions_vars(XYs,Zs).
area_boxes_positions_(ProductList,Ps,Zs) :-
box(W, H, Cost), % finding a suitable box with a W & H
%% minimize(Cost),
maplist(area_box_pos_combined(W*H),ProductList,Ps,Cs), % Setting up constraints for each product
disjoint2(Cs), % making sure they dont overlap with other product inside the box
positions_vars(Ps,Zs).
A possible query which asks to pack 4 products (numbers 2, 1, 3 and 5)
area_boxes_positions_([prod(2),prod(1),prod(3),prod(5)],Positions,Zs),
labeling([ffc],Zs).
Gives the following as output, one possible way to pack the products:
Positions = [3+1, 1+1, 4+1, 1+3],
Zs = [3, 1, 1, 1, 4, 1, 1, 3] .
But how do I model multiple boxes, when we would have an order with more products that would not fit inside one box?
Any help or examples are really appreciated!

I'm especially struggling with how to handle the unknown number of Boxes (bins).
You can put an upper bound on the number of boxes: For N indivisible elements you will never need more than N boxes. Furthermore, we can define a special "unused" kind of box with 0 size but 0 cost. Then we can ask for a solution with an assignment of items to exactly N (or any other number of) boxes, some of which can remain unused.
Here is a description of a single box, relating its kind, size, and cost using disjunctive and conjunctive constraints:
kind_width_length_cost(Kind, Width, Length, Cost) :-
% unused box
(Kind #= 0 #/\ Width #= 0 #/\ Length #= 0 #/\ Cost #= 0) #\/
% small box
(Kind #= 1 #/\ Width #= 4 #/\ Length #= 4 #/\ Cost #= 4) #\/
% medium box
(Kind #= 2 #/\ Width #= 4 #/\ Length #= 6 #/\ Cost #= 6) #\/
% large box
(Kind #= 3 #/\ Width #= 5 #/\ Length #= 5 #/\ Cost #= 7) #\/
% X-large box
(Kind #= 4 #/\ Width #= 9 #/\ Length #= 9 #/\ Cost #= 9),
% make sure all variables have finite domains, the above disjunction is
% not enough for the system to infer this
Kind in 0..4,
Width in 0..9,
Length in 0..9,
Cost in 0..9.
A collection of N boxes can be represented as a term boxes(Numbers, Kinds, Widths, Lengths, Costs) where Numbers are [1, 2, ..., N] and the I-th element of each of the other lists is the length/width/cost of box number I:
n_boxes(N, boxes(Numbers, Kinds, Widths, Lengths, Costs)) :-
numlist(1, N, Numbers),
length(Kinds, N),
maplist(kind_width_length_cost, Kinds, Widths, Lengths, Costs).
For example, three boxes are:
?- n_boxes(3, Boxes).
Boxes = boxes([1, 2, 3], [_G9202, _G9205, _G9208], [_G9211, _G9214, _G9217], [_G9220, _G9223, _G9226], [_G9229, _G9232, _G9235]),
_G9202 in 0..4,
_G9202#=4#<==>_G9257,
_G9202#=3#<==>_G9269,
_G9202#=2#<==>_G9281,
_G9202#=1#<==>_G9293,
_G9202#=0#<==>_G9305,
... a lot more constraints
Note that this uses a term containing lists rather than the more "usual" representation with a list containing terms box(Num, Width, Length, Cost). The reason for this is that we will want to index into these lists of FD variables using element/3. This predicate cannot be used to index into lists of other terms.
Turning to products, here is the FD version of your disjunctive product_either_way predicate:
product_either_way_fd(Number, Width, Length) :-
product_width_length(Number, W, L),
(Width #= W #/\ Length #= L) #\/ (Width #= L #/\ Length #= W),
% make sure Width and Length have finite domains
Width #>= min(W, L),
Width #=< max(W, L),
Length #>= min(W, L),
Length #=< max(W, L).
The placement of an item is expressed with a term box_x_y_w_l containing the number of the box, the X and Y coordinates inside the box, and the item's width and length. The placement must be compatible with the dimensions of the chosen box:
product_placement(Widths, Lengths, Number, Placement) :-
product_either_way_fd(Number, W, L),
Placement = box_x_y_w_l(_Box, _X, _Y, W, L),
placement(Widths, Lengths, Placement).
placement(Widths, Lengths, box_x_y_w_l(Box, X, Y, W, L)) :-
X #>= 0,
X + W #=< Width,
Y #>= 0,
Y + L #=< Length,
element(Box, Widths, Width),
element(Box, Lengths, Length).
This is where we use the Widths and Lengths lists of FD variables. The number of the chosen box is itself an FD variable that we use as an index to look up the box's width and length using the element/3 constraint.
Now we must model non-overlapping placements. Two items placed in different boxes are automatically non-overlapping. For two items in the same box we must check their coordinates and sizes. This binary relation must be applied to all unordered pairs of items:
placement_disjoint(box_x_y_w_l(Box1, X1, Y1, W1, L1),
box_x_y_w_l(Box2, X2, Y2, W2, L2)) :-
Box1 #\= Box2 #\/
(Box1 #= Box2 #/\
(X1 #>= X2 + W2 #\/ X1 + W1 #< X2) #/\
(Y1 #>= Y2 + L2 #\/ Y1 + L1 #< Y2)).
alldisjoint([]).
alldisjoint([Placement | Placements]) :-
maplist(placement_disjoint(Placement), Placements),
alldisjoint(Placements).
Now we are ready to put everything together. Given a list of products and a number N of boxes (some of which may be unused), the following predicate computes constraints on placements in boxes, the kinds of boxes used, their costs, and a total cost:
placements_(Products, N, Placements, BoxKinds, Costs, Cost) :-
n_boxes(N, boxes(_BoxNumbers, BoxKinds, Widths, Lengths, Costs)),
maplist(product_placement(Widths, Lengths), Products, Placements),
alldisjoint(Placements),
sum(Costs, #=, Cost).
This constructs a term representing N boxes, computes placement constraints for each product, ensures the placements are disjoint, and sets up the computation of the total cost. That is all!
I'm using the following products copied from the question. Note that I have removed duplicates with swapped widths/lengths since this swapping is done by product_either_way_fd when needed.
product_width_length(1, 2, 2).
product_width_length(2, 1, 2).
product_width_length(3, 1, 3).
product_width_length(4, 3, 3).
product_width_length(5, 2, 3).
product_width_length(6, 4, 2).
We're ready for testing. To reproduce your example of placing items 2, 1, 3, and 5 in a single box:
?- placements_([2, 1, 3, 5], 1, Placements, Kinds, Costs, Cost).
Placements = [box_x_y_w_l(1, _G17524, _G17525, _G17526, _G17527), box_x_y_w_l(1, _G17533, _G17534, 2, 2), box_x_y_w_l(1, _G17542, _G17543, _G17544, _G17545), box_x_y_w_l(1, _G17551, _G17552, _G17553, _G17554)],
Kinds = [_G17562],
Costs = [Cost],
_G17524 in 0..8,
_G17524+_G17526#=_G17599,
_G17524+_G17526#=_G17611,
_G17524+_G17526#=_G17623,
...
With labeling:
?- placements_([2, 1, 3, 5], 1, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
Placements = [box_x_y_w_l(1, 0, 0, 1, 2), box_x_y_w_l(1, 7, 7, 2, 2), box_x_y_w_l(1, 4, 6, 3, 1), box_x_y_w_l(1, 2, 3, 2, 3)],
Kinds = [4],
Costs = [9],
Cost = 9,
Variables = [0, 0, 1, 2, 7, 7, 4, 6, 3|...] .
(You might want to check this carefully for correctness!) Everything was placed in box number 1, which is of kind 4 (size 9x9) with cost 9.
Is there a way to fit these items in a cheaper box?
?- Cost #< 9, placements_([2, 1, 3, 5], 1, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
false.
Now, how about putting all the products in (up to) 6 boxes?
?- placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
Placements = [box_x_y_w_l(1, 0, 0, 2, 2), box_x_y_w_l(1, 3, 3, 1, 2), box_x_y_w_l(1, 5, 6, 1, 3), box_x_y_w_l(2, 0, 0, 3, 3), box_x_y_w_l(2, 4, 4, 2, 3), box_x_y_w_l(3, 0, 0, 2, 4)],
Kinds = [4, 4, 1, 0, 0, 0],
Costs = [9, 9, 4, 0, 0, 0],
Cost = 22,
Variables = [1, 0, 0, 1, 3, 3, 1, 2, 1|...] .
The first solution found uses three boxes and left the other three unused. Can we go cheaper?
?- Cost #< 22, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
Cost = 21,
Placements = [box_x_y_w_l(1, 0, 0, 2, 2), box_x_y_w_l(1, 3, 3, 1, 2), box_x_y_w_l(1, 5, 6, 1, 3), box_x_y_w_l(2, 0, 0, 3, 3), box_x_y_w_l(3, 0, 0, 2, 3), box_x_y_w_l(4, 0, 0, 2, 4)],
Kinds = [4, 1, 1, 1, 0, 0],
Costs = [9, 4, 4, 4, 0, 0],
Variables = [1, 0, 0, 1, 3, 3, 1, 2, 1|...] .
Yes! This solution uses more boxes, but ones that are overall slightly cheaper. Can we do even better?
?- Cost #< 21, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
% ... takes far too long
We need to be a bit more sophisticated. Playing around with the number of boxes it's clear that cheaper solutions with fewer boxes are available:
?- Cost #< 21, placements_([1, 2, 3, 4, 5, 6], 2, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
Cost = 18,
Placements = [box_x_y_w_l(1, 0, 0, 2, 2), box_x_y_w_l(1, 3, 3, 1, 2), box_x_y_w_l(1, 5, 6, 1, 3), box_x_y_w_l(2, 0, 6, 3, 3), box_x_y_w_l(2, 6, 4, 3, 2), box_x_y_w_l(2, 4, 0, 2, 4)],
Kinds = [4, 4],
Costs = [9, 9],
Variables = [1, 0, 0, 1, 3, 3, 1, 2, 1|...] .
Maybe directing the search to label box kinds first is useful, since the up strategy will essentially try to use as few boxes as possible:
?- Cost #< 21, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 35,031,786 inferences, 2.585 CPU in 2.585 seconds (100% CPU, 13550491 Lips)
Cost = 15,
Placements = [box_x_y_w_l(5, 2, 4, 2, 2), box_x_y_w_l(6, 8, 7, 1, 2), box_x_y_w_l(6, 5, 6, 3, 1), box_x_y_w_l(6, 2, 3, 3, 3), box_x_y_w_l(6, 0, 0, 2, 3), box_x_y_w_l(5, 0, 0, 2, 4)],
Kinds = [0, 0, 0, 0, 2, 4],
Costs = [0, 0, 0, 0, 6, 9],
Variables = [5, 2, 4, 6, 8, 7, 1, 2, 6|...] .
This really does need ff or ffc, the default leftmost strategy doesn't return results in a reasonable time frame.
Can we do even better?
?- Cost #< 15, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 946,355,675 inferences, 69.984 CPU in 69.981 seconds (100% CPU, 13522408 Lips)
false.
No! The solution with cost 15 is optimal (but not unique).
However, I find 70 seconds to be too slow for this very small problem size. Are there some some symmetries we can exploit? Consider:
?- Cost #= 15, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 8,651,030 inferences, 0.611 CPU in 0.611 seconds (100% CPU, 14163879 Lips)
Cost = 15,
Placements = [box_x_y_w_l(5, 2, 4, 2, 2), box_x_y_w_l(6, 8, 7, 1, 2), box_x_y_w_l(6, 5, 6, 3, 1), box_x_y_w_l(6, 2, 3, 3, 3), box_x_y_w_l(6, 0, 0, 2, 3), box_x_y_w_l(5, 0, 0, 2, 4)],
Kinds = [0, 0, 0, 0, 2, 4],
Costs = [0, 0, 0, 0, 6, 9],
Variables = [5, 2, 4, 6, 8, 7, 1, 2, 6|...] .
?- Kinds = [4, 2, 0, 0, 0, 0], Cost #= 15, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 11,182,689 inferences, 0.790 CPU in 0.790 seconds (100% CPU, 14153341 Lips)
Kinds = [4, 2, 0, 0, 0, 0],
Cost = 15,
Placements = [box_x_y_w_l(1, 7, 7, 2, 2), box_x_y_w_l(1, 6, 5, 1, 2), box_x_y_w_l(2, 3, 3, 1, 3), box_x_y_w_l(2, 0, 0, 3, 3), box_x_y_w_l(1, 4, 2, 2, 3), box_x_y_w_l(1, 0, 0, 4, 2)],
Costs = [9, 6, 0, 0, 0, 0],
Variables = [1, 7, 7, 1, 6, 5, 1, 2, 2|...] .
These aren't permutations of the same solution, but they are permutations of the same boxes and therefore have identical costs. We don't need to consider both of them! In addition to labeling Kinds a bit more intelligently than in the beginning, we can also require the Kinds list to be monotonically increasing. This excludes lots of redundant solutions and gives much faster termination, and even with better solutions first:
?- placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), chain(Kinds, #=<), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 34,943,765 inferences, 2.865 CPU in 2.865 seconds (100% CPU, 12195550 Lips)
Placements = [box_x_y_w_l(5, 2, 4, 2, 2), box_x_y_w_l(6, 8, 7, 1, 2), box_x_y_w_l(6, 5, 6, 3, 1), box_x_y_w_l(6, 2, 3, 3, 3), box_x_y_w_l(6, 0, 0, 2, 3), box_x_y_w_l(5, 0, 0, 2, 4)],
Kinds = [0, 0, 0, 0, 2, 4],
Costs = [0, 0, 0, 0, 6, 9],
Cost = 15,
Variables = [5, 2, 4, 6, 8, 7, 1, 2, 6|...] .
?- Cost #< 15, placements_([1, 2, 3, 4, 5, 6], 6, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), chain(Kinds, #=<), time(( labeling([], Kinds), labeling([ff], Variables) )).
% 31,360,608 inferences, 2.309 CPU in 2.309 seconds (100% CPU, 13581762 Lips)
false.
More tweaks are possible and probably necessary for larger problem sizes. I found that adding bisect in the final labeling helps a bit. So does removing the logically redundant Box1 #= Box2 constraint in placement_disjoint/2. Finally, given the use of chain/2 to restrict Kinds, we can remove the preliminary labeling of Kinds entirely to get a nice speedup! I'm sure there's more, but for a prototype I think it's reasonable enough.
Thank you for this interesting problem!

There are some redundancies in your partial solution, maybe caused by premature optimization.
First, since you have a product_either_way/3, you should not change your input specification, adding products with same id and dimensions swapped. After all, width and height are properties you cannot swap arbitrarly in the real world, and you already have produced a predicate that takes care of this, so I have started removing such duplicates.
Second, the purpose of disjoint/2 is to compute a placement of a set of rectangles, so your area_box_pos_combined/4 and positions_vars/2 are pretty much useless.
Here is how I would approach this problem. First, write a predicate that given a list of products and a box, puts as many as possible into it, and 'returns' those that didn't fit. For instance
fill_box([P|Ps],W,H,Placed,Rs) :-
( product(P,W_i,H_i)
; product(P,H_i,W_i)
),
W_p #= W - W_i,
H_p #= H - H_i,
X_i in 0..W_p,
Y_i in 0..H_p,
U=[p(X_i, W_i, Y_i, H_i)|Placed],
disjoint2(U),
fill_box(Ps,W,H,U,Rs).
fill_box(Rs,_,_,_,Rs).
It's somewhat buggy, because it will stop at the first product it cannot place, but there could be more placeable after this. But what's important, now we can start to test if it's working, given the interaction with key concepts of CLP(FD). disjoint/2 works on bounded variables, so the domain declaration of X_i and Y_i is needed.
?- fill_box([1,1],4,2,[],R).
R = [] .
?- fill_box([1,1],3,2,[],R).
R = [1] .
Now we can provide a driver, maybe as simple as
products_placed_cost([],0).
products_placed_cost(Ps,C) :-
box(W,H,C0),
fill_box(Ps,W,H,[],Rs),
Ps\=Rs,
products_placed_cost(Rs,C1),
C #= C0+C1.
and then let Prolog generate as many solutions as it can, just order them by cost, by means of library(solution_sequences):
?- order_by([asc(C)],products_placed_cost([1,1],C)).
C = 4 ;
C = 4 ;
C = 4 ;
C = 4 ;
C = 6 ;
...
But we don't know which placements have been generated. We have to add arguments that carry back the information. Then
products_placed_cost([],[],0).
products_placed_cost(Ps,[box(W,H,C0,Q)|Qs],C) :-
box(W,H,C0),
fill_box(Ps,W,H,[],Rs,Q),
Ps\=Rs,
products_placed_cost(Rs,Qs,C1),
C #= C0+C1.
fill_box([P|Ps],W,H,Placed,Rs,[P|Qs]) :-
( product(P,W_i,H_i)
; product(P,H_i,W_i)
),
W_p #= W - W_i,
H_p #= H - H_i,
X_i in 0..W_p,
Y_i in 0..H_p,
U=[p(X_i, W_i, Y_i, H_i)|Placed],
disjoint2(U),
fill_box(Ps,W,H,U,Rs,Qs).
fill_box(Rs,_,_,_,Rs,[]).
To be true, library(clpfd) is used just as commodity, but mixed with the searching capabilities of (pure) Prolog gives us a short and declarative solution.
See the specific documentation of library(clpBNR) for a better approach.

Related

Prolog, check for specific list parameter

% adds two lists together
add_pairs(X, [], X).
add_pairs([I1, I2, I3, I4, I5], [I7, I8, I9, I10, I11], [O1, O2, O3, O4, O5]) :-
O1 is I1 + I7,
O2 is I2 + I8,
O3 is I3 + I9,
O4 is I4 + I10,
O5 is I5 + I11.
% Checks to see if the meal is satisfactory
check_sat([_, E2, E3, E4, E5], Len) :-
E2 >= 3,
E3 =< Len - 3,
E4 =< Len - 3,
E5 >= 3.
% Adds all lists in L together.
add_lists([L|[]], L).
add_lists([X, Y|T], Out) :-
add_pairs(X, Y, Sum),
append([Sum], T, TOut),
add_lists(TOut, Out).
% Calls to the functions, determining if a meal is satisfactory
satisfactory_meal(L) :-
add_lists(L, Output),
length(L, Len),
check_sat(Output, Len).
So, I'm currently working on a program where I'm given some data as input. It returns true if certain conditions are met and false otherwise.
Basically, the list represents the attributes of a meal.
[Dish_number, Organic, Has_dairy, Has_meat, Locally_sourced]
The dish number is any number and the following attributes are represented as present if 1 is found and absent if 0 is in its place.
For example:
satisfactory_meal([
[8, 0, 0, 0, 1],
[9, 1, 1, 0, 1],
[23, 1, 0, 0, 1],
[2, 1, 0, 1, 0],
[6, 0, 0, 1, 1]
]).
Returns true. Because I need 3 or more instances of Organic and Locally_sourced.
And there are less than or equal to the length of the List - 3 of Meat or Dairy.
satisfactory_meal([
[4, 1, 0, 0, 0],
[7, 0, 1, 0, 1],
[90, 0, 0, 0, 0],
[3, 0, 0, 1, 1]
]).
returns false because organic has a single instance and locally_sourced has two. Less than what's needed.
All's good and well with my current program setup... However, there's a test case I cannot return properly. The program should return false if a single meal is disliked by all.
Which looks like this:
% No one can eat dish 3
satisfactory_meal([
[1, 1, 0, 0, 1],
[3, 0, 1, 1, 1],
[5, 0, 0, 0, 1],
[7, 1, 0, 1, 0],
[9, 1, 0, 0, 1]
]).
% No one can eat dish 9
satisfactory_meal([
[1, 1, 0, 0, 0],
[3, 1, 0, 0, 1],
[5, 0, 0, 0, 1],
[7, 1, 0, 0, 1],
[9, 0, 1, 1, 1]
]).
A meal is disliked by all if the list looks like [#, 0, 1, 1, 0] or [#, 0, 1, 1, 1].
My issue is with how I'm currently handling my lists. I'm adding them all together, making it very easy to look for less than, greater than, and equal to values.
I can't arithmetically figure out how to find the instance where a dish is disliked by all. AKA [#, 0, 1, 1, 0] or [#, 0, 1, 1, 1]. Is there a way to figure it out after summing the lists together? Or do I have to create a new function and look for those instances before adding the lists together? Any help would be most appreciated as I'm close to making this program work! Thank you.
I figured it out. I looked through the data before adding it together, creating a function that looks for the lack of instances of [#, 0, 1, 1, 0] or [#, 0, 1, 1, 1].
nonmember(Arg,[Arg|_]) :-
!,
fail.
nonmember(Arg,[_|Tail]) :-
!,
nonmember(Arg,Tail).
nonmember(_,[]).
nonmember([_,0,1,1,0], L),
nonmember([_,0,1,1,1], L),

Avoid findall overflow with n-fractions problem

I am trying to print all solutions of the n-fractions problem for n=4:
:- lib(ic).
fractions(Digits) :-
Digits = [A,B,C,D,E,F,G,H,I,J,K,L],
Digits #:: 1..9,
ic:alldifferent(Digits),
X #= 10*B+C,
Y #= 10*E+F,
Z #= 10*H+I,
V #= 10*K+L,
A*Y*Z*V + D*X*Z*V + G*X*Y*V + J*X*Y*Z #= X*Y*Z*V,
A*Y #=< D*X,
D*Z #=< G*Y,
G*V #=< J*Z,
search(Digits,0,input_order,indomain,complete,[]).
When I run the query:
?- findall(Digits,fractions(Digits),List).
I get the following exception:
*** Overflow of the local/control stack!
You can use the "-l kBytes" (LOCALSIZE) option to have a larger stack.
Peak sizes were: local stack 105728 kbytes, control stack 25344 kbytes
I am thinking if there is a way to loop inside the program and print one solution each time, or I can't do that because the problem has too many solutions?
As has been pointed out, your code fails because the alldifferent(Digits) constraint is too restrictive. The digits must be allowed to occur between 1 and 2 times. In eclipse-clp, you can use constraints such as atleast/3, atmost/3, occurrences/3 or gcc/2 to express this.
Slightly off-topic: as you are using ECLiPSe's ic-solver (which can handle continuous domains), you can actually use a model much closer to the original specification, without introducing lots of multiplications:
:- lib(ic).
:- lib(ic_global).
fractions4(Digits) :-
Digits = [A,B,C,D,E,F,G,H,I,J,K,L],
Digits #:: 1..9,
A/(10*B+C) + D/(10*E+F) + G/(10*H+I) + J/(10*K+L) $= 1,
( for(I,1,9), param(Digits) do
occurrences(I, Digits, NOcc), NOcc #:: 1..2
),
lex_le([A,B,C], [D,E,F]), % lex-ordering to eliminate symmetry
lex_le([D,E,F], [G,H,I]),
lex_le([G,H,I], [J,K,L]),
labeling(Digits).
Apart from the main equality constraint (using $= instead of #= because we don't want to require integrality here), I've used occurrences/3 for the occurrence restrictions, and lexicographic ordering constraints as a more standard way of eliminating symmetry. Result:
?- findall(Ds, fractions4(Ds), Dss), length(Dss, NSol).
Dss = [[1, 2, 4, 3, 5, 6, 8, 1, 4, 9, 2, 7], [1, 2, 6, 5, 3, 9, 7, 1, 4, 8, 2, 4], [1, 2, 6, 5, 3, 9, 7, 8, 4, 9, 1, 2], [1, 2, 6, 7, 3, 9, 8, 1, 3, 9, 5, 4], [1, 2, 6, 8, 7, 8, 9, 1, 3, 9, 5, 4], [1, 3, 4, 5, 4, 6, 8, 1, 7, 9, 2, 3], [1, 3, 4, 7, 5, 6, 8, 1, 7, 9, 2, 4], [1, 3, 4, 8, 1, 7, 8, 5, 2, 9, 2, ...], [1, 3, 5, 6, 2, 8, 7, 1, 4, 9, ...], [1, 3, 6, 5, 2, 4, 7, 1, 8, ...], [1, 3, 6, 5, 3, 6, 7, 8, ...], [1, 3, 6, 5, 4, 5, 8, ...], [1, 3, 6, 5, 6, 3, ...], [1, 3, 6, 6, 5, ...], [1, 3, 6, 7, ...], [1, 3, 9, ...], [1, 3, ...], [1, ...], [...], ...]
NSol = 1384
Yes (82.66s cpu)
An added advantage of this model is that it can be quite easily turned into a generic model for arbitrary N.
Simply your predicate fails. If you remove all the constraints except alldifferent/1 and search/6 (just to understand the problem) and call ?- fractions(Digits). you get false because it's impossible to have a list with 12 elements (Digits = [A,B,C,D,E,F,G,H,I,J,K,L]) with domain for each element Digits #:: 1..9 and constraint those elements to be all different (ic:alldifferent(Digits)). 9 options for 12 elements: unsolvable. If you expand the domain up to 12 (Digits #:: 1..12), you get a solution:
?- fractions(Digits).
Digits = [2, 3, 4, 9, 7, 10, 12, 8, 5, 11, 1, 6]
Yes (94.00s cpu, solution 1, maybe more)
Then you can apply findall/3 and see other solutions...
Many clpfd implementations offer global_cardinality constraints which I use in this example. In the following I use SICStus Prolog 4.5.0:
:- use_module(library(clpfd)).
fractions(Digits) :-
Digits = [A,B,C,D,E,F,G,H,I,J,K,L],
domain(Digits, 1, 9),
global_cardinality(Digits, [1-N1,2-N2,3-N3,4-N4,5-N5,6-N6,7-N7,8-N8,9-N9]),
domain([N1,N2,N3,N4,N5,N6,N7,N8,N9], 1, 2),
X #= 10*B+C,
Y #= 10*E+F,
Z #= 10*H+I,
V #= 10*K+L,
Z*V #= ZV,
X*Y #= XY,
A*Y*ZV + D*X*ZV + G*XY*V + J*XY*Z #= XY*ZV,
X #=< Y, X #= Y #=> A #=< D, % break some symmetries
Y #=< Z, Y #= Z #=> D #=< G,
Z #=< V, Z #= V #=> G #=< J.
Sample use:
| ?- n_fractions(4,Zs), labeling([enum],Zs).
Zs = [2,1,2,9,1,8,7,3,5,6,4,5] ? ;
Zs = [2,1,3,7,1,8,9,2,6,5,4,5] ? ;
Zs = [2,1,3,7,1,8,9,2,6,6,5,4] ? ;
...
no
Using prolog-findall for collecting all solutions works out all right, too:
?- findall(Zs,(n _fractions(4,Zs), labeling([enum],Zs)), Zss),
length(Zss, N_sols).
Zss = [[2,1,2,9,1,8,7,3,5|...],
[2,1,3,7,1,8,9,2,6|...],
[2,1,3,7,1,8,9,2|...],
[2,1,3,8,1,5,7|...],
[2,1,3,8,1,6|...],
[2,1,3,9,1|...],
[2,1,3,9|...],
[2,1,4|...],
[2,1|...],
[...|...]|...],
N_sols = 1384 ? ;
no

prolog improvement of an algorithm

% SEND+MORE=MONEY
solve(VarList):-
VarList=[D,E,M,N,O,R,S,Y], % Οι μεταβλητές του προβλήματος
Digits=[0,1,2,3,4,5,6,7,8,9], % Οι τιμές των μεταβλητών (τα ψηφία)
member(D,Digits),
member(E,Digits),
member(M,Digits),
member(N,Digits), % Ανάθεση τιμών στις μεταβλητές
member(O,Digits),
member(R,Digits),
member(S,Digits),
member(Y,Digits),
M=0, S=0, % Περιορισμοί
E=D,
M=D, M=E,
N=D, N=E, N=M,
O=D, O=E, O=M, O=N,
R=D, R=E, R=M, R=N, R=O,
S=D, S=E, S=M, S=N, S=O, S=R,
Y=D, Y=E, Y=M, Y=N, Y=O, Y=R, Y=S,
S*1000+E*100+N*10+D + M*1000+O*100+R*10+E =:= M*10000+O*1000+N*100+E*10+Y.
if i decrease the number of varriables VarList. does it improves its speed?
if i S*1000+E*100+N*10+D + M*1000+O*100+R*10+E =:= M*10000+O*1000+N*100+E*10+Y
before the checks does it improve its speed?
A clpfd approach, I am putting my solution in case someone is looking into this problem.
:- use_module( library( clpfd)).
puzzle(X):-
X=([S,E,N,D]+[M,O,R,E]=[M,O,N,E,Y]),
Vars=[S,E,N,D,M,O,R,Y],Vars ins 0..9,
S*1000 + E*100 + N*10 + D +
M*1000 + O*100 + R*10 + E #=
M*1000 + O*1000 + N*100 + E*10 + Y,
S#\=0, M#\=0,
all_different(Vars),
labeling([],Vars).
?- puzzle(X).
X = ([1, 8, 0, 5]+[4, 2, 7, 8]=[4, 2, 0, 8, 3])
X = ([1, 8, 0, 5]+[6, 2, 7, 8]=[6, 2, 0, 8, 3])
X = ([1, 8, 0, 5]+[9, 2, 7, 8]=[9, 2, 0, 8, 3])
X = ([1, 8, 0, 6]+[3, 2, 7, 8]=[3, 2, 0, 8, 4])
X = ([1, 8, 0, 6]+[5, 2, 7, 8]=[5, 2, 0, 8, 4])
X = ([1, 8, 0, 6]+[9, 2, 7, 8]=[9, 2, 0, 8, 4])
X = ([2, 7, 0, 4]+[5, 3, 6, 7]=[5, 3, 0, 7, 1])....
No, if you move the line
S*1000+E*100+N*10+D + M*1000+O*100+R*10+E =:= M*10000+O*1000+N*100+E*10+Y
above what you call "Περιορισμοί" ("restrictions", according to Google Translate), it will only become slower because it will needlessly perform the arithmetic calculations which would have been avoided with the restrictions weeding out the illegal digits assignments first.
You also have erroneous equations S = 0, M = 0, E = D, ... when it should have been S =\= 0, M =\= 0, E =\= D, ..., since all the digits in these numbers are required to be unique and the first digits in the numbers can't be zeroes.
Overall your code's speed can be improved, by reducing the domain of available values with each choice of a digit value, using select/3, instead of making all the choices from the same unaffected domain Digits with member/2. This will much reduce the combinatorial choices space, and all the digits picked will be different by construction obviating the inequality checks. The tag cryptarithmetic-puzzle's info page and Q&A entries should have more discussion and / or examples of this technique (also, the tag zebra-puzzle).

How does this Prolog program resolve to H=2? I don't understand the line of execution

I have the following chunk of Prolog taken from a YouTube tutorial on Prolog:
change(H, Q, D, N, P) :-
member(H, [0, 1, 2]),
member(Q, [0, 1, 2, 3, 4]),
member(D, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
member(N, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]),
S is 50*H + 25*Q + 10*D + 5*N,
S =< 100,
P is 100-S.
It's a program to make change on a dollar. H is half dollars, Q is quarters, D for dimes, N for nickels, P for pennies.
If I type change(H, 0, 0, 0, 0). as a query, it resolves to H=2. In the video, he mentions this is a program that makes change for $1, so I understand that two half dollars are $1, but I don't understand how it gets that.
My understanding of Prolog is that when I pass change(H, 0, 0, 0, 0)., it looks to find a value for H that will satisfy the conditions, so it goes to the first line and sees that 0 would work, then for the other "member" lines sees that the 0s that were passed also are correct.
It then sets S to a value, which given the above values would be S = 0. The next line makes sure it's less than or equal to 100, which 0 is, then sets P to 100-S (which is 100).
How is it not done there with H = 0? What am I missing?
member(H,[0,1,2]) binds H to either 0, 1 or 2. Since Q, D, N and P are all 0, the only value for H that will satisfy the equations at the bottom is 2.
When H=0, S will be 0, 100-S will be 100, and since P is 0, P is 100-S will fail.
When H=1, S will be 50, 100-S will be 50, and since P is 0, P is 100-S will fail.
When H=2, S will be 100, 100-S will be 0, and since P is 0, P is 100-S will succeed.
In addition to the operational explanation, I would like to suggest CLP(FD) constraints for such problems, which are easier to understand and more declarative than lower-level arithmetic predicates. For example, in SICStus Prolog, YAP and SWI:
:- use_module(library(clpfd)).
change(H, Q, D, N, P) :-
H in 0..2,
Q in 0..4,
D in 0..10,
N in 0..20,
S #= 50*H + 25*Q + 10*D + 5*N,
S #=< 100,
P #= 100-S.
Let us now reason declaratively:
If H = 0, as you ask, and the other parameters are 0, as you specified, then what are admissible values of P?
?- change(0, 0, 0, 0, P).
P = 100.
From this, we see that if all other arguments are 0, then the only valid solution for your query is P = 100. Thus, the goal change(0, 0, 0, 0, 0) will certainly fail.

8 queen solution that use a space state "graph" in Prolog don't work

I am studying Prolog on Ivan Bratko book: Programming for Artificial Intelligence and on the book I have found this version of 8 Queens problem that use a space state "graph" to solve the problem:
s(Queens, [Queen|Queens]) :- member(Queen, [1,2,3,4,5,6,7,8]),
noattack(Queen, Queens).
goal([_,_,_,_,_,_,_,_]).
noattack(_,[],_).
noattack(Y,[Y1|Ylist],Xdist) :-
Y1-Y =\= Xdist,
Y-Y1 =\= Xdist,
Dist1 is Xdist + 1,
noattack(Y,Ylist,Dist1).
solve(N,[N]) :- goal(N).
solve(N, [N|Sol1]) :- s(N,N1),
solve(N1,Sol1).
It combines the 8 Queens problem solution based on permutation (use its noattack/3 relation) and the s/2 predicate that I think that build the possible successor states state (the nodes of my graph). So I have something like:
s(ActualState, SuccessorState)
The goal/1 predicate I think that only specify that I have to place exactly 8 queens.
On the book say me that executing this query: solve([],Solution) it will produce a list of board positions with increasing number of queens and that this list will end with a safe configuration of the eight queens.
But if I try to execute this query don't work and I will obtain this output:
?- solve([],Solution).
ERROR: s/2: Undefined procedure: noattack/2
ERROR: However, there are definitions for:
ERROR: noattack/3
Because,rightly, the noattack predicate called in the line 2 take only 2 parameters but the noattack predicate must have 3 parameters...bue on the book is given in this wrong way and I don't know how solve this problem...
Why? What am I missing?
few bugs:
s(Queens, [Queen|Queens]) :- member(Queen, [1,2,3,4,5,6,7,8]),
noattack(Queen, Queens, 1).
noattack(_,[],_) :- !.
noattack(Y,[Y1|Ylist],Xdist) :- Y =\= Y1,
Y1-Y =\= Xdist,
Y-Y1 =\= Xdist,
Dist1 is Xdist + 1,
noattack(Y,Ylist,Dist1).
Then,
18 ?- solve([],_X), last(_X,S).
S = [4, 2, 7, 3, 6, 8, 5, 1] ;
S = [5, 2, 4, 7, 3, 8, 6, 1] ;
S = [3, 5, 2, 8, 6, 4, 7, 1] ;
S = [3, 6, 4, 2, 8, 5, 7, 1] ;
S = [5, 7, 1, 3, 8, 6, 4, 2] ;
S = [4, 6, 8, 3, 1, 7, 5, 2]
and,
25 ?- findall( X, solve([],X), _S), length(_S,N).
N = 92.

Resources