Trovare tutte le combinazioni di poliomino liberi all'interno di un'area specifica con un solutore SAT (Python)


15

Sono nuovo nel mondo dei solutori di SAT e avrei bisogno di una guida per quanto riguarda il seguente problema.

Considerando che:

❶ Ho una selezione di 14 celle adiacenti in una griglia 4 * 4

❷ Ho 5 poliomino (A, B, C, D, E) delle dimensioni 4, 2, 5, 2 e 1

❸ questi poliomino sono liberi , cioè la loro forma non è fissa e può formare modelli diversi

inserisci qui la descrizione dell'immagine

Come posso calcolare tutte le possibili combinazioni di questi 5 poliomino liberi all'interno dell'area selezionata (celle in grigio) con un solutore SAT?

Prendendo in prestito sia la risposta perspicace di @ spinkus sia la documentazione degli strumenti OR, ho potuto creare il seguente codice di esempio (eseguito in un 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)

inserisci qui la descrizione dell'immagine

Il problema è che ho codificato 5 poliomino unici / fissi e non so come definire i vincoli in modo da tenere conto di ogni possibile modello per ciascun poliomino (purché sia ​​possibile).


Ho sentito parlare degli strumenti OR di Google per la prima volta. E 'possibile utilizzare le librerie di Python standard come itertools, numpy, networkx?
Mathfux,

Preferirei usare un solutore satellitare, o strumenti preferibilmente.
solub

@solub è abbastanza facile modellare / risolvere questo tipo di problema usando il linguaggio MiniZinc, poiché ci sono vincoli di alto livello per posizionare oggetti irregolari su una superficie. Se segui il corso gratuito "Modellazione avanzata per l'ottimizzazione discreta" su Coursera , ti verrà effettivamente insegnato come farlo e ti verranno forniti alcuni esempi pratici (e più complessi). Or-Tools ha un'interfaccia per il linguaggio MiniZinc, quindi puoi ancora sfruttare la sua potenza per trovare una soluzione rapida.
Patrick Trentin,

1
Sembra interessante, grazie per il puntatore. Non sono sicuro che risponderà al problema specifico che ho (definendo i vincoli che coinvolgono poliomino liberi, non statici) ma sicuramente lo darò un'occhiata.
solub

1
Devo scusarmi, mi ero completamente dimenticato di questa domanda. C'è stata una domanda correlata nel minizinctag con una risposta dettagliata che copre il mio precedente suggerimento sull'uso minizinc.
Patrick Trentin,

Risposte:


10

EDIT: ho perso la parola "libero" nella risposta originale e ho dato la risposta usando OR-Tools per i poliomino fissi. Aggiunta una sezione a cui rispondere per includere una soluzione per poliomino liberi - che AFAICT risulta piuttosto difficile da esprimere con precisione nella programmazione dei vincoli con OR-Tools.

POLIOMINO FISSI CON OR-TOOLS:

Sì, puoi farlo con la programmazione dei vincoli in OR-Tools. OR-Tools non sa nulla della geometria della griglia 2D, quindi devi codificare la geometria di ogni forma che hai in termini di vincoli posizionali. Vale a dire una forma è una raccolta di blocchi / celle che devono avere una certa relazione tra loro, devono trovarsi entro i limiti della griglia e non devono sovrapporsi. Una volta che hai il tuo modello di vincolo, chiedi semplicemente a risolutore CP-SAT di risolverlo, nel tuo caso, per tutte le possibili soluzioni.

Ecco una prova del concetto davvero semplice con due forme rettangolari su una griglia 4x4 (probabilmente vorrai anche aggiungere un qualche tipo di codice dell'interprete per passare dalle descrizioni delle forme a un insieme di variabili e vincoli OR-Tools in un problema di scala più grande poiché introdurre i vincoli a mano è un po 'noioso).

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

dà:

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

POLYOMINOES GRATUITI:

Se consideriamo la griglia di celle come un grafico, il problema può essere reinterpretato come trovare una k-partizione delle celle della griglia in cui ogni partizione ha una dimensione specifica e inoltre ogni partizione è un componente collegato . Vale a dire AFAICT non c'è differenza tra un componente collegato e un poliomino e il resto di questa risposta fa questa ipotesi.

Trovare tutte le possibili "partizioni k delle celle della griglia in cui ogni partizione ha una dimensione specifica" è piuttosto banale da esprimere nella programmazione del vincolo OR-Tools. Ma la parte della connessione è difficile AFAICT (ho provato e fallito per un bel po '...). Penso che la programmazione dei vincoli di OR-Tools non sia l'approccio giusto. Ho notato che il riferimento C ++ di OR-Tools per le librerie di ottimizzazione della rete ha alcune cose sui componenti collegati che potrebbero valere la pena dare un'occhiata, ma non ne ho familiarità. D'altra parte, l'ingenua soluzione di ricerca ricorsiva in Python è piuttosto fattibile.

Ecco una soluzione ingenua "a mano". È piuttosto lento ma è sopportabile per la tua custodia 4x4. Gli indirizzi vengono utilizzati per identificare ogni cella nella griglia. (Nota anche che la pagina della wiki allude a qualcosa come questo algoritmo come una soluzione ingenua e sembra che suggerisca alcuni più efficienti per problemi di poliomino simili).

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

dà:

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

Questo è molto utile, grazie mille. Una cosa che è problematica è che il tuo esempio funziona solo per i poliomino di forme fisse, la domanda riguarda i poliomino liberi (numero fisso di celle ma con forme diverse, la domanda verrà modificata per chiarezza). Seguendo il tuo esempio, dovremmo codificare tutte le forme possibili (+ rotazioni + riflessioni) per ogni poliomino di dimensione S ... che non è praticabile. Rimangono le domande, è possibile implementare tali vincoli con gli strumenti OR?
solub

Oh, mancava la parte "libera". Hmmm, beh, il problema può essere messo "trova una 5 partizione di un 25-omino in cui il 25-omino è vincolato a una griglia WxH, e ciascuna delle 5 partizioni è anche X-omino per X = (7,6,6 , 4,2) .. ". Immagino sia possibile farlo con OR-Tools ma sembra che sarebbe più semplice implementare la profondità di tracciamento del CSP prima di cercare direttamente questo: Trova 25-ominos possibili. Per ogni possibile 25-omino esegui una ricerca CSP di backtrack scegliendo una X che costruisce un X-omino all'interno del 25 domino, fino a quando non trovi una soluzione completa o devi tornare indietro.
Spinkus,

Aggiunta qualcosa come l'ingenua soluzione basata sulla ricerca diretta a cui ho accennato nel commento precedente per completezza.
Spinkus,

5

Un modo relativamente semplice per vincolare una regione semplicemente connessa in OR-Tools è quello di vincolare il suo confine come circuito . Se tutti i tuoi polominomi devono avere dimensioni inferiori a 8, non dobbiamo preoccuparci di quelli non semplicemente collegati.

Questo codice trova tutte le 3884 soluzioni:

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

Per ogni polionomino e ogni possibile cella in alto a sinistra, hai una variabile booleana che indica se questa cella è la parte in alto a sinistra del rettangolo racchiuso.

Per ogni cella e ogni poliomino, hai una variabile booleana che indica se questa cella è occupata da questo poliomino.

Ora, per ogni cellula e ogni poliomino, hai una serie di implicazioni: la cella in alto a sinistra è selezionata implica che ogni cellula è effettivamente occupata da questo poliomino.

Quindi i vincoli: per ogni cella, al massimo un poliomino lo occupa per ogni poliomino, c'è esattamente una cella che è la sua parte in alto a sinistra.

questo è un puro problema booleano.


Grazie mille per la risposta ! Onestamente non ho idea di come implementarlo con or-tools, c'è qualche esempio (dagli esempi disponibili di Python forniti) che mi suggeriresti in particolare per aiutarmi a iniziare?
solub

Mi dispiace davvero perché non capisco davvero la tua risposta. Non sono sicuro a quale "rettangolo racchiuso" si riferisca o in che modo "per ogni cella e ogni poliomino" verrebbe tradotto in codice (ciclo "per" annidato?). Ad ogni modo, ti dispiacerebbe dirmi se la tua spiegazione affronta il caso dei poliomino liberi (la domanda è stata modificata per chiarezza).
solub
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.