Packing rectangles in a fixed size rectangle
Asked Answered
F

0

3

In this question, we consider how to pack a collection of polyominos into a rectangle of a fixed size.

In my case, I'm concerned with the special case where all polyominos are rectangles and while the problem is clearly still hard (it has subset sum as a special case), I am wondering if this additional structure is enough to come up with more efficient solutions than the excellent SAT/MILP-based ones that are given in the original questions.

A lot of questions already exist on rectangle packing, but most seem concerned with either letting the size of the target vary, or ensuring that all rectangles from the available set are used. Here, we just want to take exactly the same approach as in the beforementioned question. That is, we

  • have a target rectangle of fixed size,
  • we have a collection of rectangles we can place, placement is without overlap, and each rectangle can be placed multiple times (although the case where each rectangle can only be placed once is also of interest, and can be used to solve the other case by having each rectangle repeated sufficiently many times), but we don't have to use all of them (unlike in e.g. what Korf calls the "containment problem"),
  • we want to minimize the area of the gap after placement.

For example, if the target rectangle is 25x25 and the placeable rectangles are 5x4, 6x7, and 3x10, the area of the gap in an optimal solution is 13. This can be verified by using e.g. the integer programming solution with inputs of the form

dims = [(5, 4), (6, 7), (3, 10)]
tiles = [[(x, y) for x in range(dimx) for y in range(dimy)] for dimx, dimy in dims]

enter image description here

A solution is found relatively quickly but this approach struggles with larger inputs (with, say, the same placeable rectangles but a 89x89 target). Can we do better?

One thing I considered was using Z3 and introducing variables corresponding to the location of each rectangle (as opposed to having variables for each location and each rectangle type, thus reducing the total number of variables at the expense of making their ranges larger and constraints more complicated). I.e. do something like the below. This works but is much slower than the answers in the linked question.

from z3 import *

# Example input
dims = [(5, 4), (5, 4), (6, 7), (3, 10)]
n = len(dims)
areas = [x * y for x, y in dims]
rows = 20
cols = 20

s = Optimize()

# Introduce a few variables: active[i] is True iff rectangle i is in use,
# and (xs[i], ys[i]) are its coordinates
actives = [Bool(f'active{i}') for i in range(n)]
xs = [Int(f'x{i}') for i in range(n)]
ys = [Int(f'y{i}') for i in range(n)]

# Require that all rectangles are within the bounds
for i, (x, y) in enumerate(dims):
    s.add(xs[i] >= 0, xs[i] <= rows - x)
    s.add(ys[i] >= 0, ys[i] <= cols - y)

# Help the solver a bit by breaking the symmetry: If two consecutive
# rectangles are identical, only use the second if the first is in use
for i, (d1, d2) in enumerate(zip(dims, dims[1:])):
    if d1 == d2:
        s.add(Implies(actives[i + 1], actives[i]))

# Disallow overlap
for i in range(n):
    for j in range(i + 1, n):
        (xi, yi), (xj, yj) = dims[i], dims[j]
        # Introduce a variable that's true iff rectangles i and j do not overlap
        freex = Or(xs[i] >= xs[j] + xj, xs[j] >= xs[i] + xi)
        freey = Or(ys[i] >= ys[j] + yj, ys[j] >= ys[i] + yi)
        free = Or(freex, freey)
        # If rectangles i and j are in use, require them to not overlap
        s.add(Implies(And(actives[i], actives[j]), free))

# The objective is the sum of the areas of all included rectangles
obj = Sum([actives[i] * areas[i] for i in range(n)])
s.maximize(obj)
s.check()

Here, an alternative to using Optimizer is to use Solver and keep adding constraints based on the current best solution until none exist; i.e. replace the last three lines with:

while s.check() != unsat:
    model = s.model()
    covered = sum(is_true(model[actives[i]]) * areas[i] for i in range(n))
    print('Objective:', rows*cols - covered)
    s.add(obj > covered)

Using the equivalent-ish formulation with MIP solver Xpress also doesn't help much:

actives = [xp.var(vartype=xp.binary) for i in range(n)]
xs = [xp.var(lb=0, ub=rows - dims[i][0], vartype=xp.integer) for i in range(n)]
ys = [xp.var(lb=0, ub=cols - dims[i][1], vartype=xp.integer) for i in range(n)]
p.addVariable(actives, xs, ys)

for i, (d1, d2) in enumerate(zip(dims, dims[1:])):
    if d1 == d2:
        p.addConstraint(actives[i] >= actives[i+1])

for i in range(n):
    for j in range(i + 1, n):
        (xi, yi), (xj, yj) = dims[i], dims[j]
        c1 = xs[i] - xs[j] - xj
        c2 = xs[j] - xs[i] - xi
        c3 = ys[i] - ys[j] - yj
        c4 = ys[j] - ys[i] - yi
        # Use max to represent disjunction
        p.addConstraint(xp.max(-actives[i], -actives[j], c1, c2, c3, c4) >= 0)

obj = -xp.Sum([areas[i] * actives[i] for i in range(n)])
p.setObjective(obj)
p.solve()
Flyn answered 17/4, 2022 at 7:0 Comment(6)
Perhaps this can help you - github.com/juj/RectangleBinPackCisco
+1 to @IVOGELOV's suggestion that a custom solver can benefit from custom data structures that exploit problem structure just as you mentioned. An example is that the gp solver has no way of exploiting 8-way symmetry.Singband
Did you try delayed constraint generation with MIP as well? Also, for MIP solvers your symmetry breaking may not help as much as you may think. MIP solvers have their own symmetry breaking strategies that sometimes work better than things implemented by additional constraints. So it might be worth trying without them. Finally, your idea to model the overlap using max will introduce additional auxiliary variables (for linearization). I am not sure how many. If you formulate this explicitly, you can do with two binaries per pair of rects. Maybe check the model the solver actually solves.Condon
@DanielJunglas: Adding constraints along the way could be interesting indeed. And yes, plenty of new variables introduced; what's the formulation you have in mind? If you can boil it down to two binaries, I guess it's not simply using the 6 ones I have and adding big-Ms?Flyn
What I meant is two additional binaries for each pair of rectangles. Two rectangles can have four different positions relative to each other (left-right, above-below). This can be encoded in 2 binary variables. While thinking about this I also realized that could have stronger constraints on rectangles of the same type: given that these are interchangeable, you can not only require that they are used with increasing index, but also that their x-coordinates are non-decreasing (or their y-coordinates, but not both). I am wondering whether this allows the solver to run faster.Condon
The positions of the top left corners you mean? (Because that doesn't strike me as sufficient: A rectangle could have its top left corner be placed to the right and above the top left corner of another one, yet be overlapping or not overlapping.)Flyn

© 2022 - 2024 — McMap. All rights reserved.