Je suis nouveau dans le monde des solveurs SAT et aurais besoin de quelques conseils concernant le problème suivant.
Étant donné que:
❶ J'ai une sélection de 14 cellules adjacentes dans une grille 4 * 4
❷ J'ai 5 polyominos (A, B, C, D, E) de tailles 4, 2, 5, 2 et 1
❸ ces polyominos sont libres , c'est-à-dire que leur forme n'est pas fixe et peuvent former des motifs différents
Comment puis-je calculer toutes les combinaisons possibles de ces 5 polyominos libres à l'intérieur de la zone sélectionnée (cellules en gris) avec un solveur SAT?
En empruntant à la fois à la réponse perspicace de @ spinkus et à la documentation des outils OR, je pourrais créer l'exemple de code suivant (s'exécute dans un bloc-notes 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)
Le problème est que j'ai codé en dur 5 polyominos uniques/fixes et je ne sais pas comment définir les contraintes pour que chaque motif possible pour chaque polyomino soit pris en compte (à condition que cela soit possible).
Une manière relativement simple de contraindre une région simplement connectée dans OR-Tools est de contraindre sa frontière à être un circuit . Si tous vos polyominos doivent avoir une taille inférieure à 8, nous n'avons pas à nous soucier de ceux qui ne sont pas simplement connectés.
Ce code trouve toutes les 3884 solutions:
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)
Pour chaque polyonomino et chaque cellule en haut à gauche possible, vous avez une variable booléenne qui indique si cette cellule est la partie supérieure gauche du rectangle englobant.
Pour chaque cellule et chaque polyomino, vous avez une variable booléenne qui indique si cette cellule est occupée par ce polyomino.
Maintenant, pour chaque cellule et chaque polyomino, vous avez une série d'implications: la cellule en haut à gauche est sélectionnée implique que chaque cellule est effectivement occupée par ce polyomino.
Puis les contraintes: pour chaque cellule, au plus un polyomino l'occupe pour chaque polyomino, il y a exactement une cellule qui est sa partie supérieure gauche.
c'est un problème purement booléen.