SAT-solver (Python)를 사용하여 특정 영역 내에서 유리 폴리 아미노산의 모든 조합 찾기


15

저는 SAT 솔버의 세계에 익숙하지 않으며 다음 문제에 관한 지침이 필요합니다.

고려해 보면:

❶ 4 * 4 그리드에서 14 개의 인접 셀을 선택했습니다.

❷ 크기가 4, 2, 5, 2 및 1 인 5 개의 폴리 노 미노 (A, B, C, D, E)가 있습니다

poly이 폴리 아미노는 자유 롭습니다 . 즉 모양이 고정되어 있지 않고 다른 패턴을 형성 할 수 있습니다

여기에 이미지 설명을 입력하십시오

SAT 솔버를 사용하여 선택한 영역 내에서 5 개의 자유 폴리 아미노산 (회색 셀) 의 가능한 모든 조합을 어떻게 계산할 수 있습니까?

@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 도구에 대해 처음 듣습니다. 이 같은 표준 파이썬 라이브러리를 사용할 수 있습니다 itertools, numpy,를 networkx?
mathfux

sat-solver 또는 도구를 사용하는 것이 좋습니다.
solub

@solub 표면에 불규칙한 객체를 배치하기위한 높은 수준의 제약이 있기 때문에 MiniZinc 언어를 사용하여 이러한 종류의 문제를 모델링 / 해결하는 것은 매우 쉽습니다. Coursera 에서 무료 과정 인 "고급 모델링을위한 고급 모델링"을 진행하는 경우 실제로이를 수행하는 방법을 배우고 실용적인 (더 복잡한) 예제를 제공합니다. Or-Tools에는 MiniZinc 언어 용 인터페이스가 있으므로 빠른 솔루션을 찾기 위해 여전히 강력한 기능을 활용할 수 있습니다.
패트릭 트 렌틴

1
포인터 주셔서 감사합니다. 그것이 내가 가지고있는 특정 문제 (정지가 아닌 자유 폴리 아미노와 관련된 제약 조건을 정의 함)에 답할지는 확실하지 않지만 분명히 살펴볼 것입니다.
solub

1
사과해야합니다.이 질문에 대해 완전히 잊었습니다. 이 있었다 관련된 질문minizinc사용에 대한 내 이전 제안을 커버하는 상세한 답변과 함께 태그 minizinc.
패트릭 트 렌틴

답변:


10

편집 : 나는 "무료" 라는 단어를 놓쳤다 원래 답변에서 고정 폴리 아미노에 OR 도구를 사용하여 답변했습니다. AFAICT는 OR-Tools를 사용한 구속 조건 프로그래밍에서 정확하게 표현하기가 매우 어려운 것으로 밝혀진 자유 폴리오 미노에 대한 솔루션을 포함하는 섹션을 추가했습니다.

OR 도구를 사용한 고정 폴리곤 :

예 , OR-Tools에서 제약 조건 프로그래밍 으로 할 수 있습니다 . OR-Tools는 2D 그리드 지오메트리에 대해 전혀 알지 못하므로 위치 구속 조건 측면에서 각 모양의 지오메트리를 인코딩해야합니다. 즉, 모양은 서로 특정 관계를 가져야하고 격자의 경계 내에 있어야하며 겹치지 않아야하는 블록 / 셀의 모음입니다. 구속 조건 모델이 있으면 CP-SAT Solver 에게 문의하십시오. 에 가능한 모든 솔루션에 대해 해결 .

다음은 4x4 그리드에 두 개의 직사각형 모양을 가진 개념에 대한 간단한 증거입니다 (일부 해석기 코드를 추가하여 모양 설명에서 일련의 OR- 도구 변수 및 제약 조건으로 이동하는 것이 좋습니다) 제약 조건을 직접 입력하는 것은 약간 지루하기 때문에).

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 ++ 참조에는 연결된 구성 요소 에 대한 몇 가지 내용이 있지만 살펴볼 가치가 있음을 알았습니다. 반면에 파이썬의 순진 재귀 검색 솔루션은 꽤 가능합니다.

다음은 "손으로"순진한 솔루션입니다. 꽤 느리지 만 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 도구를 사용하여 이러한 제약 조건을 구현할 수 있습니까?
solub

아, "무료"부분을 놓쳤다. 음, 문제는 "25-omino가 WxH 그리드로 제한되는 25-omino의 5- 파티션을 찾을 수 있으며 X = (7,6,6)에 대해 5 개의 파티션도 X-omino입니다. , 4,2) .. ". 나는 OR 도구에서 할 수 있다고 생각하지만 CSP 역 추적 깊이 우선 검색을 직접 구현하는 것이 더 쉬울 것 같습니다. 가능한 25- 아미노를 찾으십시오. 완전한 솔루션을 찾거나 역 추적해야 할 때까지 25 개의 도미노에서 25 개의 도미노 내에 X-omino를 구축하는 X를 선택하여 역 추적 CSP 검색을 수행하십시오.
spinkus

완전성을 위해 이전 의견에서 언급 한 순진한 직접 검색 기반 솔루션과 같은 것을 추가했습니다.
spinkus

5

OR-Tools에서 간단하게 연결된 영역을 제한하는 비교적 간단한 방법은 경계를 회로 로 제한하는 것 입니다. 모든 polyominos의 크기가 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

각 polyonomino 및 가능한 각 왼쪽 상단 셀에 대해이 셀이 둘러싸는 사각형의 왼쪽 상단 부분인지 나타내는 부울 변수가 있습니다.

각 셀과 각 polyomino에 대해이 셀이이 polyomino에 의해 사용되는지 여부를 나타내는 부울 변수가 있습니다.

이제 각 셀과 각 polyomino에 대해 일련의 영향이 있습니다. 왼쪽 상단 셀이 선택되면 각 셀이 실제로이 polyomino에 의해 채워짐을 의미합니다.

그런 다음 제약 조건 : 각 셀마다 최대 하나의 폴리오 미노가 각 폴리오 미노에 대해 점유합니다. 왼쪽 상단에 정확히 하나의 셀이 있습니다.

이것은 순수한 부울 문제입니다.


답장을 보내 주셔서 감사합니다! 나는 솔직히 or-tools로 이것을 구현하는 방법을 모른다. 특히 시작하는 데 도움이되는 제안 (제공된 파이썬 예제에서)이 있습니까?
solub

당신의 대답을 정말로 이해하지 못해서 정말 죄송합니다. 어떤 "포괄 사각형"이 무엇을 의미하는지 또는 어떻게 "각 셀과 각각의 polyomino"가 코드로 번역 될 것인지 확실하지 않습니다 ( 'for'루프가 필요합니까?). 어쨌든 당신의 설명이 무료 폴리 아미노의 경우를 다루고 있는지 말해 주시겠습니까 (질문은 명확하게 편집되었습니다).
solub
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.