Tìm tất cả các kết hợp của đa giác miễn phí trong một khu vực cụ thể với bộ giải SAT (Python)


15

Tôi chưa quen với thế giới của người giải SAT và sẽ cần một số hướng dẫn về vấn đề sau.

Xem xét rằng:

Tôi có một lựa chọn 14 ô liền kề trong lưới 4 * 4

❷ Tôi có 5 polyominoes (A, B, C, D, E) có kích thước 4, 2, 5, 2 và 1

Poly những đa giác này là miễn phí , tức là hình dạng của chúng không cố định và có thể tạo thành các mẫu khác nhau

nhập mô tả hình ảnh ở đây

Làm cách nào tôi có thể tính toán tất cả các kết hợp có thể có của 5 đa giác miễn phí này bên trong khu vực được chọn (các ô màu xám) với bộ giải SAT?

Mượn cả từ câu trả lời sâu sắc của @ spinkus và tài liệu OR-tools tôi có thể tạo mã ví dụ sau (chạy trong Notebook Jupyter):

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)

nhập mô tả hình ảnh ở đây

Vấn đề là tôi đã mã hóa 5 đa giác duy nhất / cố định và tôi không biết cách xác định các ràng buộc để mỗi mẫu có thể có cho mỗi polyomino được tính đến (miễn là có thể).


Tôi nghe về Google OR-tools lần đầu tiên. Là nó có thể sử dụng thư viện Python chuẩn như itertools, numpy, networkx?
mathfux

Tôi thích sử dụng một bộ giải sat, hay công cụ tốt hơn.
solub

@solub khá dễ dàng để mô hình hóa / giải quyết loại vấn đề này bằng ngôn ngữ MiniZinc, vì có các ràng buộc cấp cao để đặt các đối tượng không đều trên bề mặt. Nếu bạn xem qua khóa học miễn phí "Mô hình hóa nâng cao để tối ưu hóa rời rạc" trên Coursera , bạn thực sự sẽ được dạy cách thực hiện và đưa ra một số ví dụ thực tế (và phức tạp hơn). Or-Tools có giao diện cho ngôn ngữ MiniZinc, vì vậy bạn vẫn có thể khai thác sức mạnh của nó để tìm giải pháp nhanh chóng.
Patrick Trentin

1
Có vẻ thú vị, cảm ơn cho con trỏ. Không chắc chắn nó sẽ trả lời vấn đề cụ thể mà tôi có (xác định các ràng buộc liên quan đến đa giác tự do, không tĩnh) nhưng tôi chắc chắn sẽ xem xét nó.
solub

1
Tôi phải xin lỗi, tôi đã hoàn toàn quên mất câu hỏi này. Đã có một câu hỏi liên quan trong minizincthẻ với câu trả lời chi tiết bao gồm đề xuất trước đây của tôi về việc sử dụng minizinc.
Patrick Trentin

Câu trả lời:


10

EDIT: Tôi đã bỏ lỡ từ "miễn phí" trong câu trả lời ban đầu và đã trả lời bằng OR-Tools cho các đa giác cố định. Đã thêm một phần để trả lời để bao gồm một giải pháp cho các đa giác miễn phí - điều mà AFAICT tỏ ra khá khó khăn để diễn đạt chính xác trong lập trình ràng buộc với OR-Tools.

POLYOMINOES CỐ ĐỊNH VỚI CÔNG CỤ HOẶC:

Vâng, bạn có thể làm điều đó với lập trình ràng buộc trong OR-Tools. OR-Tools không biết gì về hình học lưới 2D, do đó bạn phải mã hóa hình dạng của từng hình dạng mà bạn có về các ràng buộc vị trí. Tức là một hình dạng là một tập hợp các khối / ô phải có mối quan hệ nhất định với nhau, phải nằm trong giới hạn của lưới và không được trùng nhau. Khi bạn có mô hình ràng buộc của mình, bạn chỉ cần yêu cầu Bộ giải CP-SAT giải quyết nó, trong trường hợp của bạn, cho tất cả các giải pháp có thể.

Dưới đây là một bằng chứng khái niệm thực sự đơn giản với hai hình dạng hình chữ nhật trên lưới 4x4 (có lẽ bạn cũng muốn thêm một số loại mã trình thông dịch để đi từ mô tả hình dạng đến một tập hợp các biến OR-Tools và các ràng buộc trong một vấn đề quy mô lớn hơn vì việc nhập các ràng buộc bằng tay là một chút tẻ nhạt).

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()

Cung cấp:

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

POLYOMINO MIỄN PHÍ:

Nếu chúng ta coi lưới ô là một biểu đồ, vấn đề có thể được giải thích lại là tìm phân vùng k của các ô trong đó mỗi phân vùng có kích thước cụ thể và ngoài ra mỗi phân vùng là một thành phần được kết nối . Tức là AFAICT không có sự khác biệt giữa một thành phần được kết nối và một polyomino và phần còn lại của câu trả lời này đưa ra giả định đó.

Việc tìm tất cả "phân vùng k của các ô của lưới trong đó mỗi phân vùng có kích thước cụ thể" khá đơn giản để thể hiện trong lập trình ràng buộc OR-Tools. Nhưng phần kết nối là AFAICT cứng (Tôi đã thử và thất bại khá lâu ...). Tôi nghĩ lập trình ràng buộc OR-Tools không phải là phương pháp phù hợp. Tôi nhận thấy tham chiếu OR-Tools C ++ cho các thư viện tối ưu hóa mạng có một số nội dung về các thành phần được kết nối có thể đáng xem, nhưng tôi không quen với nó. Mặt khác, giải pháp tìm kiếm đệ quy ngây thơ trong Python là khá khả thi.

Đây là một giải pháp ngây thơ "bằng tay". Nó khá chậm nhưng có thể chịu được cho trường hợp của bạn. Địa chỉ được sử dụng để xác định từng ô trong lưới. (Cũng lưu ý loại trang wiki ám chỉ một thứ như thuật toán này là một giải pháp ngây thơ và có vẻ như nó gợi ý một số cách hiệu quả hơn cho các vấn đề polyomino tương tự).

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()

Cung cấp:

...
-  -  -  -
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

Điều này rất hữu ích, cảm ơn bạn rất nhiều. Một vấn đề là ví dụ của bạn chỉ hoạt động đối với các đa giác có hình dạng cố định, câu hỏi là về đa giác tự do (số lượng ô cố định nhưng với hình dạng khác nhau, câu hỏi sẽ được chỉnh sửa cho rõ ràng). Theo ví dụ của bạn, chúng tôi sẽ phải mã hóa cứng mọi hình dạng có thể (+ phép quay + phản xạ) cho mỗi polyomino có kích thước S ... không khả thi. Các câu hỏi vẫn còn, liệu có thể thực hiện các ràng buộc như vậy với các công cụ OR không?
solub

Oh một phần bỏ lỡ "miễn phí". Hmmm, vấn đề có thể được đặt ra là "tìm 5 phân vùng của 25 omino trong đó 25 omino bị ràng buộc với lưới WxH và mỗi 5 phân vùng cũng là X-omino cho X = (7.6,6 , 4.2) .. ". Tôi đoán có thể thực hiện trong OR-Tools nhưng có vẻ như sẽ dễ dàng hơn nếu chỉ thực hiện tìm kiếm độ sâu theo dõi trở lại CSP trước tiên: Tìm 25-ominos có thể. Đối với mỗi 25-omino có thể thực hiện tìm kiếm CSP quay lại bằng cách chọn X xây dựng X-omino trong 25 domino, cho đến khi bạn tìm thấy giải pháp hoàn chỉnh hoặc phải quay lại.
spinkus

Đã thêm một cái gì đó giống như giải pháp dựa trên tìm kiếm trực tiếp ngây thơ mà tôi đã đề cập trong nhận xét trước đây về tính đầy đủ.
spinkus

5

Một cách tương đối đơn giản để hạn chế một vùng được kết nối đơn giản trong OR-Tools là buộc đường viền của nó là một mạch . Nếu tất cả các polyominos của bạn có kích thước nhỏ hơn 8, chúng ta không cần phải lo lắng về những cái không được kết nối đơn giản.

Mã này tìm thấy tất cả 3884 giải pháp:

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

Đối với mỗi polyonomino và mỗi ô trên cùng bên trái có thể, bạn có một biến boolean cho biết nếu ô này là phần trên cùng bên trái của hình chữ nhật kèm theo.

Đối với mỗi ô và mỗi polyomino, bạn có một biến boolean cho biết liệu ô này có bị chiếm bởi polyomino này hay không.

Bây giờ, đối với mỗi ô và mỗi polyomino, bạn có một loạt các hàm ý: ô trên cùng bên trái được chọn ngụ ý mỗi ô thực sự bị chiếm bởi polyomino này.

Sau đó, các ràng buộc: đối với mỗi ô, nhiều nhất một polyomino chiếm nó cho mỗi polyomino, có chính xác một ô là phần trên cùng bên trái của nó.

đây là một vấn đề boolean thuần túy.


Cảm ơn bạn rất nhiều vì đã trả lời ! Tôi thực sự không biết làm thế nào để thực hiện điều này với or-tools, có ví dụ nào (từ các ví dụ python có sẵn được cung cấp) mà bạn muốn đề xuất cụ thể để giúp tôi bắt đầu không?
solub

Tôi thực sự xin lỗi vì tôi không thực sự hiểu câu trả lời của bạn. Không chắc chắn "hình chữ nhật kèm theo" đang đề cập đến hoặc làm thế nào "cho mỗi ô và mỗi polyomino" sẽ được dịch theo mã (lồng 'cho' vòng lặp?). Dù sao đi nữa, bạn có phiền khi nói với tôi nếu lời giải thích của bạn giải quyết trường hợp đa giác miễn phí (câu hỏi đã được chỉnh sửa cho rõ ràng).
solub
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.