SAT çözücü (Python) ile belirli bir alandaki tüm serbest poliomino kombinasyonlarını bulma


15

SAT çözücüler dünyasında yeniyim ve aşağıdaki sorunla ilgili bazı rehberliklere ihtiyacım var.

Hesaba katıldığında:

* 4 * 4 ızgarada 14 bitişik hücre seçimim var

4 4, 2, 5, 2 ve 1 boyutlarında 5 poliomino (A, B, C, D, E) var

Poly bu polimomlar serbesttir , yani şekilleri sabit değildir ve farklı desenler oluşturabilir

resim açıklamasını buraya girin

Seçilen 5 alandaki (gri renkli hücreler) bu 5 serbest poliominonun olası tüm kombinasyonlarını bir SAT çözücü ile nasıl hesaplayabilirim ?

Hem @ spinkus'un anlayışlı cevabından hem de OR araçları belgelerinden ödünç alarak aşağıdaki örnek kodu yapabilirim (Jupyter Notebook'da çalışır):

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)

resim açıklamasını buraya girin

Sorun, sabit kodlu 5 benzersiz / sabit poliominoya sahip olmam ve kısıtlamaları nasıl tanımlayacağımı bilmiyorum, böylece her bir poliomino için olası her desen dikkate alındığında (mümkün olduğu takdirde).


Google OR araçlarını ilk kez duyuyorum. Bu gibi standart Python kütüphanelerini kullanmak mümkün mü itertools, numpy, networkx?
mathfux

Tercihen bir sat çözücü veya araç kullanmayı tercih ederim.
Ocak'ta

@solub MiniZinc dilini kullanarak bu tür bir sorunu modellemek / çözmek oldukça kolaydır, çünkü düzensiz nesneleri bir yüzeye yerleştirmek için üst düzey kısıtlamalar vardır. Coursera'da ücretsiz "Kesikli Optimizasyon için Gelişmiş Modelleme" dersinden geçerseniz, aslında nasıl yapılacağı öğretilecek ve bazı pratik (ve daha karmaşık) örnekler verilecektir. Or-Tools'un MiniZinc dili için bir arayüzü vardır, bu nedenle hızlı bir çözüm bulmak için gücünü kullanmaya devam edebilirsiniz.
Patrick Trentin

1
İlginç görünüyor, işaretçi için teşekkürler. Sahip olduğum özel soruna cevap vereceğinden emin değilim (statik olmayan, serbest poliominoları içeren kısıtlamaları tanımlayan), ancak kesinlikle bir göz atacağım.
çözünürlüğü

1
Özür dilemeliyim, bu soruyu tamamen unutmuştum. Bir yaşandı ilgili soru içinde minizinckullanma hakkında önceki öneride kapsayan detaylı bir cevabı olan etiketi minizinc.
Patrick Trentin

Yanıtlar:


10

DÜZENLEME: Orijinal yanıtta "ücretsiz" kelimesini kaçırdım ve sabit polimomlar için OR-Tools kullanarak cevap verdim. AFAICT'in OR-Tools ile kısıt programlamasında tam olarak ifade edilmesinin oldukça zor olduğu ortaya çıkan ücretsiz poliominolar için bir çözüm içerecek bir cevap bölümü eklendi.

OR ARAÇLARI İLE SABİT POLİOMİNOLAR:

Evet bunu OR- Tools'da kısıt programlama ile yapabilirsiniz . OR-Tools 2D ızgara geometrisi hakkında hiçbir şey bilmiyor, bu nedenle sahip olduğunuz her şeklin geometrisini konumsal kısıtlamalar açısından kodlamanız gerekiyor. Bir şekil, birbiriyle belirli bir ilişkisi olması gereken, ızgaranın sınırları içinde olması ve üst üste gelmemesi gereken blokların / hücrelerin bir koleksiyonudur. Kısıtlama modelinize sahip olduktan sonra, CP-SAT Çözücü'den sizin durumunuzda olası tüm çözümler için çözmesini istersiniz .

İşte 4x4 ızgarada iki dikdörtgen şekle sahip gerçekten basit bir kavram kanıtı (muhtemelen daha büyük ölçekli bir problemde şekil açıklamalarından bir dizi OR-Tools değişkenine ve kısıtlamasına gitmek için bir çeşit yorumlayıcı kodu eklemek istersiniz. çünkü kısıtlamaları elle girmek biraz sıkıcıdır).

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

verir:

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

ÜCRETSİZ POLİOMİNOLAR:

Hücrelerin ızgarasını bir grafik olarak düşünürsek, sorun, her bir bölümün belirli bir boyuta sahip olduğu ve ek olarak her bölümün bağlı bir bileşen olduğu ızgaradaki hücrelerin k-bölümünü bulmak olarak yeniden yorumlanabilir . Yani AFAICT bağlı bir bileşen ve bir arasında hiçbir fark yoktur Polyomino ve bu cevabın kalanı varsayım yapar.

OR-Tools kısıtlama programlamasında "her bir bölümün belirli bir boyuta sahip olduğu ızgara hücrelerinin tüm olası k-bölümlerini bulmak oldukça önemlidir. Ama bağlantılılık kısmı zor AFAICT (Bir süre denedim ve başarısız oldum ...). Bence OR-Tools kısıtlama programlama doğru bir yaklaşım değil. Ağ optimizasyonu kütüphaneleri için OR-Tools C ++ referansının bir göz atmaya değer bağlı bileşenler üzerinde bazı şeyler olduğunu fark ettim , ama buna aşina değilim. Öte yandan, Python'da saf özyinelemeli arama çözümü oldukça yapılabilir.

İşte "elle" naif bir çözüm. Oldukça yavaş ama 4x4 kasanız için katlanılabilir. Adresler ızgaradaki her bir hücreyi tanımlamak için kullanılır. (Ayrıca wiki sayfasının bu algoritma gibi bir şeye naif bir çözüm olduğunu ve benzer polyomino problemleri için daha verimli olanlar önerdiğini unutmayın).

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

verir:

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

Bu çok faydalı, çok teşekkür ederim. Sorunlu olan bir şey, örneğinizin sadece sabit şekillerdeki polominolar için çalıştığıdır, soru serbest poliominolar hakkındadır (sabit hücre sayısı, ancak farklı şekillerle, soru netlik için düzenlenecektir). Örneğinizin ardından, S ... boyutundaki her bir poliomino için mümkün olmayan her şekli (+ dönüşler + yansımalar) sabit olarak kodlamamız gerekir. Sorular hala var, bu tür kısıtlamaları OR araçlarıyla uygulamak mümkün müdür?
40'da çözülür

Oh özledim "özgür" kısmı. Hmmm, sorun "25-omino'nun bir WxH ızgarası ile kısıtlandığı 25-omino'nun 5-bölümünü bulmak ve her 5 bölüm X için X-omino = (7,6,6) , 4,2) .. ". Sanırım OR-Tools'da yapmak mümkün, ancak CSP geri izleme derinliğini ilk önce doğrudan aramak için daha kolay olacak gibi kokuyor: Olası 25 ominos bulun. Her olası 25-omino için, tam bir çözüm bulana veya geri takip etmek zorunda kalana kadar 25 domino içinde bir X-omino inşa eden bir X seçerek bir geri izleme CSP araması yapın.
spinkus

Tam yorum için önceki yorumda bahsettiğim saf doğrudan arama tabanlı çözüm gibi bir şey ekledi.
spinkus

5

OR-Tools'da basit bağlantılı bir bölgeyi sınırlamanın nispeten basit bir yolu, sınırını bir devre olarak sınırlamaktır . Tüm poliominoslarınızın boyutu 8'den küçükse, basitçe bağlı olmayanlar için endişelenmemize gerek yoktur.

Bu kod tüm 3884 çözümlerini bulur:

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

Her bir polyonomino ve olası her sol üst hücre için, bu hücrenin çevreleyen dikdörtgenin sol üst kısmı olup olmadığını gösteren bir boolean değişkeniniz vardır.

Her hücre ve her bir polomino için, bu hücrenin bu poliomino tarafından işgal edilip edilmediğini gösteren bir boole değişkeniniz vardır.

Şimdi, her hücre ve her poliomino için bir dizi çıkarım var: sol üst hücre seçildi, her bir hücrenin aslında bu poliomino tarafından işgal edildiğini ima ediyor.

Daha sonra kısıtlamalar: her hücre için, her bir poliomino için en fazla bir polyomino işgal eder, sol üst kısmı olan tam olarak bir hücre vardır.

bu saf bir boole problemidir.


Yanıt için çok teşekkürler ! Dürüst olmak gerekirse bunu or-tools ile nasıl uygulayacağım hakkında hiçbir fikrim yok, özellikle başlamama yardımcı olmak için önerebileceğiniz herhangi bir örnek var mı (sağlanan mevcut python örneklerinden)?
çözünürlüğü

Cevabınızı gerçekten anlamadığım için gerçekten üzgünüm. Hangi "çevreleyen dikdörtgenin" neyi ifade ettiğinden veya "her hücre ve her poliomino için" kodda nasıl çevrileceğinden emin değilsiniz ('döngü? İçin yuvalanmış?). Her neyse, açıklamanızın serbest poliominoların durumunu ele alıp almadığını bana söyler misiniz (soru netlik için düzenlenmiştir).
çözünürlüğü
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.