Пошук усіх комбінацій вільних поліоміно в певній області із SAT-розв'язувачем (Python)


15

Я новачок у світі вирішувачів SAT і мені потрібні певні вказівки щодо наступної проблеми.

Враховуючи, що:

❶ У мене є вибір з 14 суміжних комірок у сітці 4 * 4

У мене є 5 поліоміно (A, B, C, D, E) розмірів 4, 2, 5, 2 і 1

Ці поліоміно є вільними , тобто їх форма не закріплена і може утворювати різні візерунки

введіть тут опис зображення

Як я можу обчислити всі можливі комбінації цих 5 вільних поліоміносів всередині вибраної області (клітини сірого кольору) за допомогою SAT-розв'язувача?

Запозичивши як проникливу відповідь @ spinkus, так і документацію щодо OR-інструментів, я міг зробити наступний приклад коду (працює в Jupyter Notebook):

from ortools.sat.python import cp_model

import numpy as np
import more_itertools as mit
import matplotlib.pyplot as plt
%matplotlib inline


W, H = 4, 4 #Dimensions of grid
sizes = (4, 2, 5, 2, 1) #Size of each polyomino
labels = np.arange(len(sizes))  #Label of each polyomino

colors = ('#FA5454', '#21D3B6', '#3384FA', '#FFD256', '#62ECFA')
cdict = dict(zip(labels, colors)) #Color dictionary for plotting

inactiveCells = (0, 1) #Indices of disabled cells (in 1D)
activeCells = set(np.arange(W*H)).difference(inactiveCells) #Cells where polyominoes can be fitted
ranges = [(next(g), list(g)[-1]) for g in mit.consecutive_groups(activeCells)] #All intervals in the stack of active cells



def main():
    model = cp_model.CpModel()


    #Create an Int var for each cell of each polyomino constrained to be within Width and Height of grid.
    pminos = [[] for s in sizes]
    for idx, s in enumerate(sizes):
        for i in range(s):
            pminos[idx].append([model.NewIntVar(0, W-1, 'p%i'%idx + 'c%i'%i + 'x'), model.NewIntVar(0, H-1, 'p%i'%idx + 'c%i'%i + 'y')])



    #Define the shapes by constraining the cells relative to each other

    ## 1st polyomino -> tetromino ##
    #                              #      
    #                              # 
    #            #                 # 
    #           ###                # 
    #                              # 
    ################################

    p0 = pminos[0]
    model.Add(p0[1][0] == p0[0][0] + 1) #'x' of 2nd cell == 'x' of 1st cell + 1
    model.Add(p0[2][0] == p0[1][0] + 1) #'x' of 3rd cell == 'x' of 2nd cell + 1
    model.Add(p0[3][0] == p0[0][0] + 1) #'x' of 4th cell == 'x' of 1st cell + 1

    model.Add(p0[1][1] == p0[0][1]) #'y' of 2nd cell = 'y' of 1st cell
    model.Add(p0[2][1] == p0[1][1]) #'y' of 3rd cell = 'y' of 2nd cell
    model.Add(p0[3][1] == p0[1][1] - 1) #'y' of 3rd cell = 'y' of 2nd cell - 1



    ## 2nd polyomino -> domino ##
    #                           #      
    #                           # 
    #           #               # 
    #           #               # 
    #                           # 
    #############################

    p1 = pminos[1]
    model.Add(p1[1][0] == p1[0][0])
    model.Add(p1[1][1] == p1[0][1] + 1)



    ## 3rd polyomino -> pentomino ##
    #                              #      
    #            ##                # 
    #            ##                # 
    #            #                 # 
    #                              #
    ################################

    p2 = pminos[2]
    model.Add(p2[1][0] == p2[0][0] + 1)
    model.Add(p2[2][0] == p2[0][0])
    model.Add(p2[3][0] == p2[0][0] + 1)
    model.Add(p2[4][0] == p2[0][0])

    model.Add(p2[1][1] == p2[0][1])
    model.Add(p2[2][1] == p2[0][1] + 1)
    model.Add(p2[3][1] == p2[0][1] + 1)
    model.Add(p2[4][1] == p2[0][1] + 2)



    ## 4th polyomino -> domino ##
    #                           #      
    #                           # 
    #           #               #   
    #           #               # 
    #                           # 
    #############################

    p3 = pminos[3]
    model.Add(p3[1][0] == p3[0][0])
    model.Add(p3[1][1] == p3[0][1] + 1)



    ## 5th polyomino -> monomino ##
    #                             #      
    #                             # 
    #           #                 # 
    #                             # 
    #                             # 
    ###############################
    #No constraints because 1 cell only



    #No blocks can overlap:
    block_addresses = []
    n = 0
    for p in pminos:
        for c in p:
            n += 1
            block_address = model.NewIntVarFromDomain(cp_model.Domain.FromIntervals(ranges),'%i' % n)
                model.Add(c[0] + c[1] * W == block_address)
                block_addresses.append(block_address)

    model.AddAllDifferent(block_addresses)



    #Solve and print solutions as we find them
    solver = cp_model.CpSolver()

    solution_printer = SolutionPrinter(pminos)
    status = solver.SearchForAllSolutions(model, solution_printer)

    print('Status = %s' % solver.StatusName(status))
    print('Number of solutions found: %i' % solution_printer.count)




class SolutionPrinter(cp_model.CpSolverSolutionCallback):
    ''' Print a solution. '''

    def __init__(self, variables):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self.variables = variables
        self.count = 0

    def on_solution_callback(self):
        self.count += 1


        plt.figure(figsize = (2, 2))
        plt.grid(True)
        plt.axis([0,W,H,0])
        plt.yticks(np.arange(0, H, 1.0))
        plt.xticks(np.arange(0, W, 1.0))


        for i, p in enumerate(self.variables):
            for c in p:
                x = self.Value(c[0])
                y = self.Value(c[1])
                rect = plt.Rectangle((x, y), 1, 1, fc = cdict[i])
                plt.gca().add_patch(rect)

        for i in inactiveCells:
            x = i%W
            y = i//W
            rect = plt.Rectangle((x, y), 1, 1, fc = 'None', hatch = '///')
            plt.gca().add_patch(rect)

введіть тут опис зображення

Проблема полягає в тому, що у мене жорстко закодовані 5 унікальних / фіксованих поліоміно, і я не знаю, як визначити обмеження, щоб враховувати кожен можливий зразок для кожного поліоміно (за умови, що це можливо).


Я чую про Google OR-інструменти вперше. Чи можна використовувати стандартні бібліотеки Python , такі як itertools, numpy, networkx?
mathfux

Я вважаю за краще використовувати сіт-розв'язувач або інструменти.
солюб

@solub моделювати / вирішувати подібну проблему досить просто, використовуючи мову MiniZinc, оскільки існують обмеження високого рівня для розміщення неправильних предметів на поверхні. Якщо ви пройдете безкоштовний курс "Розширене моделювання дискретної оптимізації" на Coursera , вас насправді навчать, як це зробити, та наведіть кілька практичних (і більш складних) прикладів. Or-Tools має інтерфейс для мови MiniZinc, тому ви все ще можете використовувати його потужність для швидкого пошуку рішення.
Патрік Трентін

1
Здається, цікаво, дякую за вказівник. Не впевнений, що це відповість на конкретну проблему, яку я маю (визначаючи обмеження, пов’язані з вільними полиоминозами, а не статичними), але я обов'язково буду розглядати це.
солюб

1
Треба вибачитися, я зовсім забув про це питання. Там був пов'язаний питання в minizincтезі з розгорнутою відповіддю , який покриває моє попереднє речення про використання minizinc.
Патрік Трентін

Відповіді:


10

EDIT: Я пропустив слово "безкоштовно" в оригінальній відповіді і дав відповідь, використовуючи OR-Tools для фіксованих поліоміно. Додав розділ, щоб відповісти, щоб включити рішення для безкоштовних поліоміно, яке AFAICT виявляється досить складно виразити точно в програмуванні обмежень за допомогою OR-Tools.

ФІКСОВАНІ ПОЛІОМІНОВИ ІЗ ІНСТРУМЕНТАМИ

Так, ви можете це зробити з обмеженням програмування в OR-Tools. OR-Tools нічого не знає про 2D геометрію сітки, тому вам доведеться кодувати геометрію кожної фігури, яку ви маєте, з позиції обмежень. Тобто форма - це сукупність блоків / комірок, які повинні мати певне відношення один до одного, повинні бути в межах сітки і не повинні перетинатися. Коли у вас є модель обмеження, ви просто попросіть CP-SAT Solver вирішити її, у вашому випадку, для всіх можливих рішень.

Ось справді простий доказ концепції з двома формами прямокутника на сітці 4х4 (ви також, ймовірно, хочете додати якийсь код інтерпретатора, щоб перейти від описів фігури до набору змінних та обмежень OR-Tools у великій проблемі масштабу оскільки введення обмежень вручну є трохи стомлюючим).

from ortools.sat.python import cp_model

(W, H) = (3, 3) # Width and height of our grid.
(X, Y) = (0, 1) # Convenience constants.


def main():
  model = cp_model.CpModel()
  # Create an Int var for each block of each shape constrained to be within width and height of grid.
  shapes = [
    [
      [ model.NewIntVar(0, W, 's1b1_x'), model.NewIntVar(0, H, 's1b1_y') ],
      [ model.NewIntVar(0, W, 's1b2_x'), model.NewIntVar(0, H, 's1b2_y') ],
      [ model.NewIntVar(0, W, 's1b3_x'), model.NewIntVar(0, H, 's1b3_y') ],
    ],
    [
      [ model.NewIntVar(0, W, 's2b1_x'), model.NewIntVar(0, H, 's2b1_y') ],
      [ model.NewIntVar(0, W, 's2b2_x'), model.NewIntVar(0, H, 's2b2_y') ],
    ]
  ]

  # Define the shapes by constraining the blocks relative to each other.
  # 3x1 rectangle:
  s0 = shapes[0]
  model.Add(s0[0][Y] == s0[1][Y])
  model.Add(s0[0][Y] == s0[2][Y])
  model.Add(s0[0][X] == s0[1][X] - 1)
  model.Add(s0[0][X] == s0[2][X] - 2)
  # 1x2 rectangle:
  s1 = shapes[1]
  model.Add(s1[0][X] == s1[1][X])
  model.Add(s1[0][Y] == s1[1][Y] - 1)

  # No blocks can overlap:
  block_addresses = []
  for i, block in enumerate(blocks(shapes)):
    block_address = model.NewIntVar(0, (W+1)*(H+1), 'b%d' % (i,))
    model.Add(block[X] + (H+1)*block[Y] == block_address)
    block_addresses.append(block_address)
  model.AddAllDifferent(block_addresses)

  # Solve and print solutions as we find them
  solver = cp_model.CpSolver()
  solution_printer = SolutionPrinter(shapes)
  status = solver.SearchForAllSolutions(model, solution_printer)
  print('Status = %s' % solver.StatusName(status))
  print('Number of solutions found: %i' % solution_printer.count)


def blocks(shapes):
  ''' Helper to enumerate all blocks. '''
  for shape in shapes:
    for block in shape:
      yield block


class SolutionPrinter(cp_model.CpSolverSolutionCallback):
    ''' Print a solution. '''

    def __init__(self, variables):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self.variables = variables
        self.count = 0

    def on_solution_callback(self):
      self.count += 1
      solution = [(self.Value(block[X]), self.Value(block[Y])) for shape in self.variables for block in shape]
      print((W+3)*'-')
      for y in range(0, H+1):
        print('|' + ''.join(['#' if (x,y) in solution else ' ' for x in range(0, W+1)]) + '|')
      print((W+3)*'-')


if __name__ == '__main__':
  main()

Дає:

...
------
|    |
| ###|
|  # |
|  # |
------
------
|    |
| ###|
|   #|
|   #|
------
Status = OPTIMAL
Number of solutions found: 60

БЕЗКОШТОВНІ ПОЛІОМІНОВИ:

Якщо ми розглянемо сітку комірок як графік, то проблема може бути повторно інтерпретована як пошук k-розділу комірок сітки, де кожен розділ має певний розмір, і крім того, кожен розділ є з'єднаним компонентом . Тобто AFAICT немає різниці між з'єднаним компонентом і поліоміно, а решта цієї відповіді робить це припущення.

Пошук усіх можливих "k-розділів комірок сітки, де кожен розділ має певний розмір", є досить тривіальним для вираження в програмуванні обмежень OR-Tools. Але зв'язаність частина важко AFAICT (я пробував і не довгий час ...). Я думаю, що програмування обмежень OR-Tools не є правильним підходом. Я помітив, що посилання OR + Tools C ++ для бібліотек оптимізації мережі містить деякі дані про підключені компоненти, які, можливо, варто переглянути, але я не знайомий з цим. З іншого боку, наївне рекурсивне рішення пошуку в Python є цілком можливим.

Ось наївне рішення "від руки". Це досить повільно, але терпимо для вашого корпусу 4x4. Адреси використовуються для ідентифікації кожної комірки в сітці. (Також зауважте, що на сторінці вікі натякає щось на зразок цього алгоритму як наївне рішення, і схоже, що він пропонує деякі більш ефективні для подібних проблем поліоміно).

import numpy as np
from copy import copy
from tabulate import tabulate

D = 4 # Dimension of square grid.
KCC = [5,4,2,2] # List of the sizes of the required k connected components (KCCs).
assert(sum(KCC) <= D*D)
VALID_CELLS = range(2,D*D)

def search():
  solutions = set() # Stash of unique solutions.
  for start in VALID_CELLS: # Try starting search from each possible starting point and expand out.
    marked = np.zeros(D*D).tolist()
    _search(start, marked, set(), solutions, 0, 0)
  for solution in solutions:  # Print results.
    print(tabulate(np.array(solution).reshape(D, D)))
  print('Number of solutions found:', len(solutions))

def _search(i, marked, fringe, solutions, curr_count, curr_part):
  ''' Recursively find each possible KCC in the remaining available cells the find the next, until none left '''
  marked[i] = curr_part+1
  curr_count += 1
  if curr_count == KCC[curr_part]: # If marked K cells for the current CC move onto the next one.
    curr_part += 1
    if curr_part == len(KCC): # If marked K cells and there's no more CCs left we have a solution - not necessarily unique.
      solutions.add(tuple(marked))
    else:
      for start in VALID_CELLS:
        if marked[start] == 0:
          _search(start, copy(marked), set(), solutions, 0, curr_part)
  else:
    fringe.update(neighbours(i, D))
    while(len(fringe)):
      j = fringe.pop()
      if marked[j] == 0:
        _search(j, copy(marked), copy(fringe), solutions, curr_count, curr_part)

def neighbours(i, D):
  ''' Find the address of all cells neighbouring the i-th cell in a DxD grid. '''
  row = int(i/D)
  n = []
  n += [i-1] if int((i-1)/D) == row and (i-1) >= 0 else []
  n += [i+1] if int((i+1)/D) == row and (i+1) < D**2 else []
  n += [i-D] if (i-D) >=0 else []
  n += [i+D] if (i+D) < D**2 else []
  return filter(lambda x: x in VALID_CELLS, n)

if __name__ == '__main__':
  search()

Дає:

...
-  -  -  -
0  0  1  1
2  2  1  1
4  2  3  1
4  2  3  0
-  -  -  -
-  -  -  -
0  0  4  3
1  1  4  3
1  2  2  2
1  1  0  2
-  -  -  -
Number of solutions found: 3884

Це дуже корисно, дуже дякую. Одне з проблемних питань - це те, що ваш приклад працює лише для поліоміно фіксованих форм, питання про вільні поліоміно (фіксовану кількість комірок, але різної форми, питання буде відредаговано для наочності). Слідуючи вашому прикладу, нам доведеться жорстко кодувати всі можливі форми (+ обертання + відбиття) для кожного поліоміно розміром S ..., що не піддається життєздатності. Залишається питання, чи можливо реалізувати такі обмеження за допомогою OR-інструментів?
солюб

О пропущена "вільна" частина. Гммм, ну проблему можна поставити "знайти 5-ти розділ 25-оміно, де 25-оміно обмежується мережею WxH, і кожен 5 розділів також є X-omino для X = (7,6,6 , 4,2) .. ". Я думаю, що це можливо зробити в OR-Tools, але це пахне, як було б простіше просто реалізувати глибину відстеження CSP, спочатку шукайте це безпосередньо: Знайдіть можливі 25-оміно. Для кожного можливого 25-omino виконуйте зворотний пошук CSP, вибираючи X-побудову X-omino в межах 25 доміно, поки не знайдете повне рішення або не доведеться відхилятись.
шпигун

Додано щось на кшталт наївного рішення на основі прямого пошуку, про яке я нагадав у попередньому коментарі на повноту.
шпигун

5

Один відносно простий спосіб обмеження просто з'єднаної області в OR-Tools - обмеження її межі бути ланцюгом . Якщо всі ваші поліоміно мають розмір менше 8, нам не потрібно турбуватися про не просто підключені.

Цей код знаходить усі 3884 рішення:

from ortools.sat.python import cp_model

cells = {(x, y) for x in range(4) for y in range(4) if x > 1 or y > 0}
sizes = [4, 2, 5, 2, 1]
num_polyominos = len(sizes)
model = cp_model.CpModel()

# Each cell is a member of one polyomino
member = {
    (cell, p): model.NewBoolVar(f"member{cell, p}")
    for cell in cells
    for p in range(num_polyominos)
}
for cell in cells:
    model.Add(sum(member[cell, p] for p in range(num_polyominos)) == 1)

# Each polyomino contains the given number of cells
for p, size in enumerate(sizes):
    model.Add(sum(member[cell, p] for cell in cells) == size)

# Find the border of each polyomino
vertices = {
    v: i
    for i, v in enumerate(
        {(x + i, y + j) for x, y in cells for i in [0, 1] for j in [0, 1]}
    )
}
edges = [
    edge
    for x, y in cells
    for edge in [
        ((x, y), (x + 1, y)),
        ((x + 1, y), (x + 1, y + 1)),
        ((x + 1, y + 1), (x, y + 1)),
        ((x, y + 1), (x, y)),
    ]
]
border = {
    (edge, p): model.NewBoolVar(f"border{edge, p}")
    for edge in edges
    for p in range(num_polyominos)
}
for (((x0, y0), (x1, y1)), p), border_var in border.items():
    left_cell = ((x0 + x1 + y0 - y1) // 2, (y0 + y1 - x0 + x1) // 2)
    right_cell = ((x0 + x1 - y0 + y1) // 2, (y0 + y1 + x0 - x1) // 2)
    left_var = member[left_cell, p]
    model.AddBoolOr([border_var.Not(), left_var])
    if (right_cell, p) in member:
        right_var = member[right_cell, p]
        model.AddBoolOr([border_var.Not(), right_var.Not()])
        model.AddBoolOr([border_var, left_var.Not(), right_var])
    else:
        model.AddBoolOr([border_var, left_var.Not()])

# Each border is a circuit
for p in range(num_polyominos):
    model.AddCircuit(
        [(vertices[v0], vertices[v1], border[(v0, v1), p]) for v0, v1 in edges]
        + [(i, i, model.NewBoolVar(f"vertex_loop{v, p}")) for v, i in vertices.items()]
    )

# Print all solutions
x_range = range(min(x for x, y in cells), max(x for x, y in cells) + 1)
y_range = range(min(y for x, y in cells), max(y for x, y in cells) + 1)
solutions = 0


class SolutionPrinter(cp_model.CpSolverSolutionCallback):
    def OnSolutionCallback(self):
        global solutions
        solutions += 1
        for y in y_range:
            print(
                *(
                    next(
                        p
                        for p in range(num_polyominos)
                        if self.Value(member[(x, y), p])
                    )
                    if (x, y) in cells
                    else "-"
                    for x in x_range
                )
            )
        print()


solver = cp_model.CpSolver()
solver.SearchForAllSolutions(model, SolutionPrinter())
print("Number of solutions found:", solutions)

4

Для кожної поліономіни та кожної можливої ​​верхньої лівої комірки ви маєте булеву змінну, яка вказує, чи ця клітинка є лівою верхньою частиною додаючого прямокутника.

Для кожної клітини та кожного поліоміно є булева змінна, яка вказує, чи є ця клітина зайнята цим поліоміно.

Тепер для кожної клітини та кожного полиомино ви маєте ряд наслідків: вибирається верхній лівий осередок означає, що кожна клітина фактично зайнята цим поліоміно.

Тоді обмеження: для кожної клітини, максимум, один полиомино займає її для кожного полиомино, є рівно одна клітина, яка є її верхньою лівою частиною.

це чиста булева проблема.


Дуже дякую за відповідь! Я, чесно кажучи, не маю уявлення, як це реалізувати за допомогою or-tools, чи є який-небудь приклад (із доступних прикладів python), який ви б запропонували, зокрема, щоб допомогти мені почати роботу?
солюб

Мені справді шкода, оскільки я не дуже розумію вашу відповідь. Не впевнений, на що йдеться у "прямокутнику, що охоплює", або як "для кожної комірки та кожного поліоміно" перекладався б у код (вкладений "для" циклу?). У будь-якому випадку, ви не хотіли б сказати мені, якщо ваше пояснення стосується випадку вільних поліоміно (питання було відредаговано для наочності).
солюб
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.