Обработка ограничений Prolog: квадраты упаковки

Я пытаюсь решить проблему обработки ограничений в прологе.

Мне нужно упаковать 4 квадрата 5x5,4x4,3x3 и 2x2 в сетку 10x10. Они не могут перекрываться.

Мои переменные выглядят так:

Name: SqX(i), i=1..10, domain: 1..10

Где X либо 5,4,3, либо 2. Индекс i представляет строку, домен - столбец в сетке.

Мои первые ограничения пытаются определить ширину и высоту квадратов. Я формулирую это так:

Constraint: SqX(i) > SqX(j)-X /\ i>j-X, range: i>0 /\ j>0

Таким образом, возможные точки должны находиться в пределах X строк и столбцов друг от друга. Однако Prolog останавливается на этих ограничениях и дает следующий результат:

Adding constraint "(Sq5_I > Sq5_J-5) /\ (I>J-5)" for values:
        I=1, J=1, 
        I=1, J=2, 
        I=1, J=3, 
        I=1, J=4, 
        I=1, J=5, 
        I=1, J=6, 
=======================[ End Solutions ]=======================

На этом он останавливается, даже не проверяя другие квадраты. Мои ограничения, скорее всего, слишком жесткие, но я не понимаю, почему и как. Какие-либо предложения?


person Sven    schedule 29.11.2012    source источник


Ответы (5)


Для каждого квадрата определите переменные X и Y, которые обозначают верхний левый угол. У этих переменных будут домены 1..10-L, где L - длина квадрата. Если вы установите домен на 1..10, квадраты могут быть частично размещены за пределами вашего прямоугольника 10x10.

Затем вы можете опубликовать ограничения для каждой пары прямоугольников (X,Y) и (X1,Y1), в которых указано, что если они перекрываются по оси x, они не должны перекрываться по оси Y, и наоборот:

(((X  #=< X1) and (X+L   #> X1)) => ((Y+L #=< Y1) or (Y1+L1 #=< Y))),
(((X1 #=< X)  and (X1+L1 #> X))  => ((Y+L #=< Y1) or (Y1+L1 #=< Y))),
(((Y  #=< Y1) and (Y+L   #> Y1)) => ((X+L #=< X1) or (X1+L1 #=< X))),
(((Y1 #=< Y)  and (Y1+L1 #> Y))  => ((X+L #=< X1) or (X1+L1 #=< X)))

(ваш конкретный синтаксис ограничения может отличаться)

person twinterer    schedule 29.11.2012
comment
Спасибо, значит, я все время ошибался ... Постараюсь реализовать вашу версию. Но мне было интересно, какова цель символа # в ваших ограничениях. Я не могу сразу понять его цель, и поиск в Google не помог. - person Sven; 30.11.2012
comment
Он обозначает ограничения на целые числа в ECLiPSe. Как я уже сказал, синтаксис может различаться в зависимости от используемой вами системы Prolog. - person twinterer; 30.11.2012
comment
Одно замечание, диапазон 1..10-L должен быть 1..10-(L-1) или 1..11-L. Ваше ограничение кажется излишним, но я посмотрю на него. (@Sven AI taak 2?) - person Steven Roose; 07.12.2012
comment
Действительно, AI таак. Мое окончательное решение не похоже на мою первую идею, но основано на этом ответе. - person Sven; 12.12.2012

Начиная с версии 3.8.3 SICStus Prolog предлагает ряд специальных ограничения размещения, которые точно соответствуют вашей задаче упаковки. В частности, поскольку ваша проблема упаковки двумерна, вам следует рассмотреть возможность использования ограничения disjoint2/1.

В следующем фрагменте кода disjoint2/1 используется для обозначения того, что прямоугольники не перекрываются. Основное отношение - area_boxes_positions_/4.

:- use_module(library(clpfd)).
:- use_module(library(lists)).

area_box_pos_combined(W_total*H_total,W*H,X+Y,f(X,W,Y,H)) :-
    X #>= 1,
    X #=< W_total-W+1,
    Y #>= 1,
    Y #=< H_total-H+1.

positions_vars([],[]).
positions_vars([X+Y|XYs],[X,Y|Zs]) :-
    positions_vars(XYs,Zs).

area_boxes_positions_(Area,Bs,Ps,Zs) :-
    maplist(area_box_pos_combined(Area),Bs,Ps,Cs),
    disjoint2(Cs),
    positions_vars(Ps,Zs).

Перейдем к некоторым вопросам! Во-первых, ваша первоначальная проблема с упаковкой:

?- area_boxes_positions_(10*10,[5*5,4*4,3*3,2*2],Positions,Zs),
   labeling([],Zs).
Positions = [1+1,1+6,5+6,5+9],
Zs        = [1,1,1,6,5,6,5,9] ? ...

Далее минимизируем общую площадь, необходимую для размещения всех квадратов:

?- domain([W,H],1,10),
   area_boxes_positions_(W*H,[5*5,4*4,3*3,2*2],Positions,Zs),
   WH #= W*H,
   minimize(labeling([ff],[H,W|Zs]),WH).
W         = 9,
H         = 7,
Positions = [1+1,6+1,6+5,1+6],
Zs        = [1,1,6,1,6,5,1,6],
WH        = 63 ? ...

Визуализация решений

Как на самом деле выглядят индивидуальные решения? ImageMagick может создавать красивые маленькие растровые изображения ...

Вот немного грубого кода для сброса правильной команды ImageMagick:

:- use_module(library(between)).
:- use_module(library(codesio)).

drawWithIM_at_area_name_label(Sizes,Positions,W*H,Name,Label) :-
    Pix = 20,

    % let the ImageMagick command string begin
    format('convert -size ~dx~d xc:skyblue', [(W+2)*Pix, (H+2)*Pix]),

    % fill canvas 
    format(' -stroke none -draw "fill darkgrey rectangle ~d,~d ~d,~d"', 
           [Pix,Pix, (W+1)*Pix-1,(H+1)*Pix-1]),

    % draw grid
    drawGridWithIM_area_pix("stroke-dasharray 1 1",W*H,Pix),

    % draw boxes
    drawBoxesWithIM_at_pix(Sizes,Positions,Pix),

    % print label
    write( ' -stroke none -fill black'),
    write( ' -gravity southwest -pointsize 16 -annotate +4+0'),
    format(' "~s"',[Label]),

    % specify filename
    format(' ~s~n',[Name]).

В приведенном выше коде для drawWithIM_at_area_name_label/5 используются два маленьких помощника:

drawGridWithIM_area_pix(Stroke,W*H,P) :-   % vertical lines
    write(' -strokewidth 1 -fill none -stroke gray'),
    between(2,W,X),
    format(' -draw "~s path \'M ~d,~d L ~d,~d\'"', [Stroke,X*P,P, X*P,(H+1)*P-1]),
    false.
drawGridWithIM_area_pix(Stroke,W*H,P) :-   % horizontal lines
    between(2,H,Y),
    format(' -draw "~s path \'M ~d,~d L ~d,~d\'"', [Stroke,P,Y*P, (W+1)*P-1,Y*P]),
    false.
drawGridWithIM_area_pix(_,_,_).

drawBoxesWithIM_at_pix(Sizes,Positions,P) :-
    Colors = ["#ff0000","#00ff00","#0000ff","#ffff00","#ff00ff","#00ffff"],
    write(' -strokewidth 2 -stroke white'),
    nth1(N,Positions,Xb+Yb),
    nth1(N,Sizes,    Wb*Hb),
    nth1(N,Colors,   Color),
    format(' -draw "fill ~sb0 roundrectangle ~d,~d ~d,~d ~d,~d"',
           [Color, Xb*P+3,Yb*P+3, (Xb+Wb)*P-3,(Yb+Hb)*P-3, P/2,P/2]),
    false.
drawBoxesWithIM_at_pix(_,_,_).

Использование визуализаторов

Давайте воспользуемся следующими двумя запросами, чтобы создать несколько неподвижных изображений.

?- drawWithIM_at_area_name_label([5*5,4*4,3*3,2*2],[1+1,6+1,6+5,1+6],9*7,
                                 'dj2_9x7.gif','9x7').

?- drawWithIM_at_area_name_label([5*5,4*4,3*3,2*2],[1+1,1+6,5+6,5+9],10*10,
                                 'dj2_10x10.gif','10x10').

Давайте воспользуемся следующим хак-запросом, чтобы создать изображение для каждого решения размещения вышеуказанных прямоугольников на доске размером 9*7:

?- retractall(nSols(_)), 
   assert(nSols(1)), 
   W=9,H=7,
   Boxes = [5*5,4*4,3*3,2*2],
   area_boxes_positions_(W*H,Boxes,Positions,Zs),
   labeling([],Zs), 
   nSols(N), 
   retract(nSols(_)), 
   format_to_codes('dj2_~5d.gif',[N],Name),
   format_to_codes('~dx~d: solution #~d',[W,H,N],Label),
   drawWithIM_at_area_name_label(Boxes,Positions,W*H,Name,Label),
   N1 is N+1,
   assert(nSols(N1)),
   false.

Затем выполните все команды ImageMagick, выводимые указанными выше запросами.

Наконец, с помощью ImageMagick построим анимацию набора решений третьего запроса:

$ convert -delay 15  dj2_0.*.gif   dj2_9x7_allSolutions_1way.gif 
$ convert dj2_9x7_allSolutions_1way.gif -coalesce -duplicate 1,-2-1 \
          -quiet -layers OptimizePlus -loop 0 dj2_9x7_allSolutions.gif

Полученные результаты

Во-первых, одно решение для платы размером 10 * 10: 10x10: одно решение

Во-вторых, одно решение для платы минимального размера (9 * 7): 9x7: одно решение

Наконец, все решения для платы минимального размера (9 * 7): 9x7: все решения


Изменить 2015-04-14

Начиная с версии 7.1.36, библиотека clpfd SWI-Prolog поддерживает ограничение disjoint2/1.

Изменить 2015-04-22

Вот набросок альтернативной реализации, основанной на ограничении tuples_in/2:

  1. Для каждой пары полей определите все позиции, в которых эти два поля не будут перекрываться.
  2. Кодируйте допустимые комбинации как списки кортежей.
  3. Для каждой пары ящиков разместите одно ограничение tuples_in/2.

В качестве частного доказательства концепции я реализовал некоторый код, следуя этой идее; как @CapelliC в его ответе, я получаю 169480 различных решений для ящиков и размеров платы, заявленных OP.

Время выполнения сопоставимо с другими ответами на основе clp (FD); на самом деле он очень конкурентоспособен для небольших плат (10 * 10 и меньше), но становится намного хуже для досок большего размера.

Примите к сведению, что из соображений приличия я воздерживаюсь от размещения кода :)

person repeat    schedule 01.04.2015
comment
Как можно было бы изменить этот код, если бы нам пришлось иметь дело с ячейками переменных? Итак, если бы у нас было несколько бункеров с разными фиксированными размерами? - person ebeo; 02.08.2020
comment
@ebeo Хотите разместить несколько лотков одинакового размера? Например. разместить четыре ячейки «4 * 4», три ячейки «2 * 4» и две ячейки «1 * 4»? - person repeat; 05.08.2020

Здесь уже опубликовано несколько отличных решений (+1 за всех!), Использующих ограничения CLP (FD).

Кроме того, я хотел бы показать один концептуально отличный способ решения таких задач размещения и покрытия с использованием ограничений CLP (B).

Идея состоит в том, чтобы рассматривать каждое возможное размещение плитки как набор значений ИСТИНА в определенных элементах сетки, где каждый элемент сетки соответствует одному столбцу матрицы, и каждое возможное размещение плитки соответствует одной строке. Затем задача состоит в том, чтобы выбрать набор строк указанной матрицы таким образом, чтобы каждый элемент сетки был покрыт не более одного раза, или, другими словами, в каждом столбце таблицы было не более одного значения ИСТИНА. подматрица, состоящая из выбранных строк.

В этой формулировке выбор строк и, следовательно, размещение плиток в определенных положениях указывается логическими переменными, по одной для каждой строки матрицы.

Вот код, которым я хотел бы поделиться, он работает в SICStus Prolog и SWI с минимальными изменениями:

:- use_module(library(clpb)).
:- use_module(library(clpfd)).

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   The tiles we have available for placement.

   For example, a 2x2 tile is represented in matrix form as:

       [[1,1],
        [1,1]]

   1 indicates which grid elements are covered when placing the tile.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

tile(5*5).
tile(4*4).
tile(3*3).
tile(2*2).

tile_matrix(Rows) :-
        tile(M*N),
        length(Rows, M),
        maplist(length_list(N), Rows),
        append(Rows, Ls),
        maplist(=(1), Ls).

length_list(L, Ls) :- length(Ls, L).

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   Describe placement of tiles as SAT constraints.

   Notice the use of Cards1 to make sure that each tile is used
   exactly once. Remove or change this constraint if a shape can be
   used multiple times, or can even be omitted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

placement(M, N, Vs, *(Cs) * *(Cards1)) :-
        matrix(M, N, TilesRows),
        pairs_keys_values(TilesRows, Tiles, Rows),
        same_length(Rows, Vs),
        pairs_keys_values(TilesVs0, Tiles, Vs),
        keysort(TilesVs0, TilesVs),
        group_pairs_by_key(TilesVs, Groups),
        pairs_values(Groups, SameTiles),
        maplist(card1, SameTiles, Cards1),
        Rows = [First|_],
        phrase(all_cardinalities(First, Vs, Rows), Cs).

card1(Vs, card([1], Vs)).

all_cardinalities([], _, _) --> [].
all_cardinalities([_|Rest], Vs, Rows0) -->
        { maplist(list_first_rest, Rows0, Fs, Rows),
          pairs_keys_values(Pairs0, Fs, Vs),
          include(key_one, Pairs0, Pairs),
          pairs_values(Pairs, Cs) },
        [card([0,1], Cs)],
        all_cardinalities(Rest, Vs, Rows).

key_one(1-_).

list_first_rest([L|Ls], L, Ls).

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   We build a matrix M_ij, where each row i describes what placing a
   tile at a specific position looks like: Each cell of the grid
   corresponds to a unique column of the matrix, and the matrix
   entries that are 1 indicate the grid positions that are covered by
   placing one of the tiles at the described position. Therefore,
   placing all tiles corresponds to selecting specific rows of the
   matrix such that, for the selected rows, at most one "1" occurs in
   each column.

   We represent each row of the matrix as Ts-Ls, where Ts is the tile
   that is used in each case.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

matrix(M, N, Ms) :-
        Squares #= M*N,
        length(Ls, Squares),
        findall(Ts-Ls, line(N, Ts, Ls), Ms).

line(N, Ts, Ls) :-
        tile_matrix(Ts),
        length(Ls, Max),
        phrase((zeros(0,P0),tile_(Ts,N,Max,P0,P1),zeros(P1,_)), Ls).

tile_([], _, _, P, P) --> [].
tile_([T|Ts], N, Max, P0, P) -->
        tile_part(T, N, P0, P1),
        { (P1 - 1) mod N >= P0 mod N,
          P2 #= min(P0 + N, Max) },
        zeros(P1, P2),
        tile_(Ts, N, Max, P2, P).

tile_part([], _, P, P) --> [].
tile_part([L|Ls], N, P0, P) --> [L],
        { P1 #= P0 + 1 },
        tile_part(Ls, N, P1, P).

zeros(P, P)  --> [].
zeros(P0, P) --> [0], { P1 #= P0 + 1 }, zeros(P1, P).

Следующий запрос показывает, какие элементы сетки покрываются (1), где каждая строка соответствует размещению одного из прямоугольников:

?- M = 7, N = 9, placement(M, N, Vs, Sat), sat(Sat),
  labeling(Vs), matrix(M, N, Ms), pairs_values(Ms, Rows),
  pairs_keys_values(Pairs0, Vs, Rows),
  include(key_one, Pairs0, Pairs1), pairs_values(Pairs1, Covers),
  maplist(writeln, Covers).
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1]
[0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
M = 7,
N = 9,
etc.

соответствующее решению:

решение исходной проблемы

Такая формулировка CLP (B) обычно менее масштабируема, чем версия CLP (FD), также потому, что в ней задействовано больше переменных. Однако у него также есть несколько преимуществ:

Одним из значительных преимуществ является то, что его легко обобщить для версии задачи, в которой некоторые или все формы могут использоваться несколько раз. Например, в версии выше мы можем просто изменить card1/2 на:

custom_cardinality(Vs, card([0,1,2,3,4,5,6,7], Vs)).

и получить версию, в которой каждую плитку можно использовать до 7 раз, а можно даже полностью опустить (из-за включения 0).

Во-вторых, мы можем легко превратить это в решение проблемы точного покрытия, что означает, что каждый элемент сетки покрывается одной из фигур, просто изменив card([0,1], Cs) на card([1], Cs) в all_cardinalities//3.

Вместе с другой модификацией это покрытие для сетки 4х4 из четырех прямоугольников 2х2:

[1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0]
[0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,1,1,0,0,1,1,0,0]
[0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1]

Третье преимущество формулировки CLP (B) состоит в том, что количество решений может быть вычислено без явного перечисления решений. Например, для исходной задачи:

?- placement(7, 9, Vs, Sat), sat_count(Sat, Count).
Count = 68.

Эти 68 решений уже прекрасно проиллюстрированы @repeat.

Для сравнения, вот количество решений, в которых каждую фигуру можно использовать от 0 до 7 раз:

?- placement(7, 9, Vs, Sat), time(sat_count(Sat, Count)).
% 157,970,727 inferences, 19.165 CPU in 19.571 seconds
...
Count = 17548478.

То же самое в сетке 10х10, вычисленное примерно за 6 минут (~ 2 миллиарда выводов):

?- placement(10, 10, Vs, Sat), sat_count(Sat, Count).
Count = 140547294509.

И на сетке 11x11, рассчитанной примерно за полчаса (~ 9 миллиардов выводов):

?- placement(11, 11, Vs, Sat), sat_count(Sat, Count).
Count = 15339263199580.

Наконец, что, возможно, наиболее важно, этот подход работает для плиток любой формы и не ограничивается квадратами или прямоугольниками. Например, для обработки квадратов 1x1 и формы треугольника, а также их вертикальных и горизонтальных отражений используйте следующее определение tile_matrix/1:

tile_matrix([[1]]).
tile_matrix(T) :-
        T0 = [[1,1,1,1],
              [1,1,1,0],
              [1,1,0,0],
              [1,0,0,0]],
        (   T = T0
        ;   maplist(reverse, T0, T)
        ;   reverse(T0, T)
        ).

Разрешив использовать каждую из этих форм от 0 до 7 раз на доске 9x7, я получаю через минуту или около того Count = 58665048314 решений.

Вот один из них, выбранный наугад:

пример с треугольными формами

Выбор решений таким образом, чтобы каждое из них было одинаково вероятным, также довольно просто с CLP (B), даже если количество решений слишком велико, чтобы перечислить их явно.

person mat    schedule 17.05.2015

Я закодировал на SWI-Prolog

/*  File:    pack_squares.lp
    Author:  Carlo,,,
    Created: Nov 29 2012
    Purpose: http://stackoverflow.com/questions/13623775/prolog-constraint-processing-packing-squares
*/

:- module(pack_squares, [pack_squares/0]).
:- [library(clpfd)].

pack_squares :-
    maplist(square, [5,4,3,2], Squares),
    flatten(Squares, Coords),
    not_overlap(Squares),
    Coords ins 1..10,
    label(Coords),
    maplist(writeln, Squares),
    draw_squares(Squares).

draw_squares(Squares) :-
    forall(between(1, 10, Y),
           (   forall(between(1, 10, X),
              sumpts(X, Y, Squares, 0)),
           nl
           )).

sumpts(_, _, [], S) :- write(S).
sumpts(X, Y, [[X1,Y1, X2,Y2]|Qs], A) :-
    ( ( X >= X1, X =< X2, Y >= Y1, Y =< Y2 )
    ->  B is A+X2-X1+1
    ;   B is A
    ),
    sumpts(X, Y, Qs, B).

square(D, [X1,Y1, X2,Y2]) :-
    X1 + D - 1 #= X2,
    Y1 + D - 1 #= Y2.

not_overlap([_]).
not_overlap([A,B|L]) :-
    not_overlap(A, [B|L]),
    !, not_overlap([B|L]).

not_overlap(_, []).
not_overlap(Q, [R|Rs]) :-
    not_overlap_c(Q, R),
    not_overlap_c(R, Q),
    not_overlap(Q, Rs).

not_overlap_c([X1,Y1, X2,Y2], Q) :-
    not_inside(X1,Y1, Q),
    not_inside(X1,Y2, Q),
    not_inside(X2,Y1, Q),
    not_inside(X2,Y2, Q).

not_inside(X,Y, [X1,Y1, X2,Y2]) :-
    X #< X1 #\/ X #> X2 #\/ Y #< Y1 #\/ Y #> Y2.

вот последние строки, отображаемые при запуске ?- aggregate_all(count,pack_squares,C)., в частности, C подсчитывает общее количество размещений

...
0002255555
0002255555
[6,6,10,10]
[7,2,10,5]
[4,3,6,5]
[5,1,6,2]
0000220000
0000224444
0003334444
0003334444
0003334444
0000055555
0000055555
0000055555
0000055555
0000055555
C = 169480.
person CapelliC    schedule 29.11.2012
comment
:- use_module(library(clpfd)). вместо :- [...]. - person false; 29.11.2012
comment
Спасибо, но я бы предпочел попробовать свою реализацию. - person Sven; 30.11.2012

Вот решение, в котором дизъюнкт занимает только одну строку:

% disjoint(+Rectangle, +Rectangle)
disjoint([XA1,XA2,YA1,YA2],[XB1,XB2,YB1,YB2]) :-
   XB1 #>= XA2 #\/ XA1 #>= XB2 #\/
   YB1 #>= YA2 #\/ YA1 #>= YB2.

Настройка модели и маркировка работают следующим образом:

% squares(-List)
squares(L) :-
   maplist(square, [2,3,4,5], L),
   term_variables(L, V),
   place(L),
   label(V).

% square(+Integer, -Rectangle)
square(S, [X1,X2,Y1,Y2]) :-
   X1 in 0..8,
   X2 in 1..9,
   Y1 in 0..6,
   Y2 in 1..7,
   X2 #= X1+S,
   Y2 #= Y1+S.

% place(+List)
place([]).
place([A|L]) :-
   place(L, A),
   place(L).

% place(+List, +Rectangle)
place([], _).
place([A|L], B) :-
   disjoint(A, B),
   place(L, B).

Вот пример запуска:

Jekejeke Prolog 3, Runtime Library 1.3.7 (May 23, 2019)

?- squares(L), show(L).
555554444
555554444
555554444
555554444
55555333.
22...333.
22...333.
L = [[0,2,5,7],[5,8,4,7],[5,9,0,4],[0,5,0,5]]
person Mostowski Collapse    schedule 02.06.2019