我正在尝试使用约束逻辑编程(CLP)在(Swi-)Prolog中找到针对NP硬2D可变尺寸装箱问题(2DVSBPP)的算法。
可以这样解释问题:需要将某些订购的产品尽可能有效地包装到某些 Boxs (箱)中。产品具有一定的宽度和长度(正方形或矩形,例如2x3)。有四个不同大小的盒子,每个盒子的托运人都有给定的成本(例如5x5盒子为4美元,5x7盒子为5美元)。 目标是使包装盒中的总成本降到最低。
一段时间以来,我一直在寻找解决这个问题的方法,并且阅读了许多其他语言的论文和类似示例。但是,我找不到任何有效的解决方案。 我在如何处理未知数量的盒子(箱)方面特别挣扎。
为了找到解决此问题的方法,我尝试调整a similar problem,但实际上不知道如何处理数量可变的盒子。下面的代码可以选择最便宜的盒子来容纳所有产品,只要,因为只需要一个盒子就可以容纳所有产品。从我们需要多个盒子的那一刻起,该程序就失败了。
包装盒和产品:
:- 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).
约束:
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).
可能的查询,要求包装4种产品(数字2、1、3和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] .
但是,当我们要订购更多的产品而又不能容纳在一个盒子中时,如何为多个盒子建模?
任何帮助或示例都非常感谢!
答案 0 :(得分:6)
我在如何处理未知数量的Box(垃圾箱)方面特别挣扎。
您可以对盒子的数量设置上限:对于N个不可分割的元素,您将永远不需要超过N个盒子。此外,我们可以定义一种特殊的“未使用”盒子,尺寸为0,成本为0。然后,我们可以请求一种解决方案,将项目分配给确切 N个(或其他数量的)盒子,其中一些可以保留不使用。
这里是对单个盒子的描述,它使用析取和析取约束来关联其种类,大小和成本:
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.
N个框的集合可以表示为项boxes(Numbers, Kinds, Widths, Lengths, Costs)
,其中Numbers
是[1, 2, ..., N]
,而每个其他列表的第I
个元素是长度/框号I
的/ width / cost:
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).
例如,三个框是:
?- 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
请注意,这使用包含列表的术语,而不是带有包含术语box(Num, Width, Length, Cost)
的列表的“更常用”表示形式。这样做的原因是我们希望使用element/3
索引到这些FD变量列表中。该谓词不能用于索引其他术语的列表。
转向产品,这是析取性product_either_way
谓词的FD版本:
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).
物品的放置位置用术语box_x_y_w_l
表示,其中包含盒子的编号,盒子内的X和Y坐标以及物品的宽度和长度。放置位置必须与所选框的尺寸兼容:
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).
这是我们使用FD变量的Widths
和Lengths
列表的地方。所选框的编号本身就是FD变量,我们用作索引,使用element/3
约束条件来查找框的宽度和长度。
现在,我们必须对非重叠展示位置进行建模。放在不同框中的两个项目会自动不重叠。对于同一框中的两个项目,我们必须检查其坐标和大小。此二进制关系必须应用于所有无序的项目对:
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).
现在,我们准备将所有内容放在一起。给定产品列表和N个盒子(其中一些可能未使用),以下谓词计算对盒子中的放置,所用盒子的种类,其成本和总成本的约束:
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).
这将构造一个表示N个盒子的术语,计算每个产品的放置约束,确保放置不相交,并设置总成本的计算。就是这样!
我正在使用从问题中复制的以下产品。请注意,由于这种交换是在需要时由product_either_way_fd
完成的,因此我删除了宽度/长度互换的重复项。
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).
我们已经准备好进行测试。重现将项目2、1、3和5放在一个盒子中的示例:
?- 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,
...
带有标签:
?- 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|...] .
(您可能要仔细检查它的正确性!)所有内容都放在1号框内,该框为4号(9x9尺寸),成本为9。
有没有办法将这些物品放在便宜的盒子里?
?- Cost #< 9, placements_([2, 1, 3, 5], 1, Placements, Kinds, Costs, Cost), term_variables(Placements, Variables, [Cost | Costs]), labeling([], Variables).
false.
现在,如何将所有产品放入(最多)6个盒子中?
?- 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|...] .
找到的第一个解决方案使用三个框,其余三个未使用。我们可以便宜一点吗?
?- 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|...] .
是的!此解决方案使用了 more 盒,但总体上价格略低一些。我们还能做得更好吗?
?- 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
我们需要变得更加复杂。玩弄盒子的数量,很明显,可以找到价格便宜,盒子更少的解决方案:
?- 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|...] .
也许首先将搜索定向到标签框类型很有用,因为up
策略实际上将尝试使用尽可能少的框:
?- 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|...] .
这确实需要ff
或ffc
,默认的leftmost
策略不会在合理的时间范围内返回结果。
我们还能做得更好吗?
?- 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.
不!成本为15的解决方案是最佳的(但不是唯一的)。
但是,对于这个很小的问题,我发现70秒太慢了。我们可以利用某些对称性吗?考虑:
?- 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|...] .
这些不是同一解决方案的置换,但是它们是同一 box 的置换,因此具有相同的成本。我们不需要同时考虑它们!除了比起初更智能地标记Kinds
之外,我们还可以要求Kinds
列表要单调增加。这排除了很多冗余解决方案,并提供了更快的终止速度,甚至首先使用了更好的解决方案:
?- 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.
更多的调整是可能的,对于更大的问题可能是必要的。我发现在最终标签中添加bisect
会有所帮助。在Box1 #= Box2
中删除逻辑上冗余的placement_disjoint/2
约束也是如此。最后,考虑到使用chain/2
来限制Kinds
,我们可以完全删除Kinds
的初步标签,以获得更好的加速效果!我敢肯定还有更多,但是对于原型我认为这是足够合理的。
谢谢您这个有趣的问题!
答案 1 :(得分:1)
部分解决方案中存在一些冗余,可能是由于过早的优化所致。
首先,由于您具有product_two_way / 3,因此不应更改输入规格,而应添加具有相同ID和尺寸的产品。毕竟,width和height是您在现实世界中不能任意交换的属性,并且您已经产生了一个可以解决此问题的谓词,因此我已经开始删除此类重复项。
第二,Disjoint / 2的目的是计算一组矩形的放置,因此您的area_box_pos_combined / 4和positions_vars / 2几乎没有用。
这是我如何解决此问题的方法。首先,编写一个谓词,给出给定的产品列表和包装盒,将尽可能多的产品放入其中,然后“退货”那些不合适的产品。例如
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).
这有点bug,因为它将在无法放置的第一个产品处停止,但是此后可能会放置更多。但是重要的是,考虑到与CLP(FD)关键概念的相互作用,现在我们可以开始测试它是否有效。 disjoint / 2适用于有界变量,因此需要X_i和Y_i的域声明。
?- fill_box([1,1],4,2,[],R).
R = [] .
?- fill_box([1,1],3,2,[],R).
R = [1] .
现在我们可以提供一个驱动程序,也许就这么简单
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.
然后让Prolog生成尽可能多的解决方案,只需通过库(solution_sequences)来按成本订购它们即可:
?- order_by([asc(C)],products_placed_cost([1,1],C)).
C = 4 ;
C = 4 ;
C = 4 ;
C = 4 ;
C = 6 ;
...
但是我们不知道生成了哪些展示位置。我们必须添加带回信息的参数。然后
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,[]).
确实,library(clpfd)只是作为商品使用,但与(纯)Prolog的搜索功能混合在一起,为我们提供了一个简短而声明性的解决方案。
有关更好的方法,请参见库(the specific documentation)的clpBNR。