Skip to content

Commit 319f4e2

Browse files
committed
[CP-SAT] improve no_overlap_2d cuts; more no_overlap_2d cuts; bugfixes
1 parent 2f7ebad commit 319f4e2

17 files changed

+723
-193
lines changed

ortools/sat/2d_rectangle_presolve.cc

Lines changed: 172 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -40,85 +40,14 @@
4040
namespace operations_research {
4141
namespace sat {
4242

43-
bool PresolveFixed2dRectangles(
43+
namespace {
44+
std::vector<Rectangle> FindSpacesThatCannotBeOccupied(
45+
absl::Span<const Rectangle> fixed_boxes,
4446
absl::Span<const RectangleInRange> non_fixed_boxes,
45-
std::vector<Rectangle>* fixed_boxes) {
46-
// This implementation compiles a set of areas that cannot be occupied by any
47-
// item, then calls ReduceNumberofBoxes() to use these areas to minimize
48-
// `fixed_boxes`.
49-
bool changed = false;
50-
51-
DCHECK(FindPartialRectangleIntersections(*fixed_boxes).empty());
52-
IntegerValue original_area = 0;
53-
std::vector<Rectangle> fixed_boxes_copy;
54-
if (VLOG_IS_ON(1)) {
55-
for (const Rectangle& r : *fixed_boxes) {
56-
original_area += r.Area();
57-
}
58-
}
59-
if (VLOG_IS_ON(2)) {
60-
fixed_boxes_copy = *fixed_boxes;
61-
}
62-
63-
const int original_num_boxes = fixed_boxes->size();
64-
65-
// The greedy algorithm is really fast. Run it first since it might greatly
66-
// reduce the size of large trivial instances.
67-
std::vector<Rectangle> empty_vec;
68-
if (ReduceNumberofBoxesGreedy(fixed_boxes, &empty_vec)) {
69-
changed = true;
70-
}
71-
72-
IntegerValue min_x_size = std::numeric_limits<IntegerValue>::max();
73-
IntegerValue min_y_size = std::numeric_limits<IntegerValue>::max();
74-
75-
CHECK(!non_fixed_boxes.empty());
76-
Rectangle bounding_box = non_fixed_boxes[0].bounding_area;
77-
78-
for (const RectangleInRange& box : non_fixed_boxes) {
79-
bounding_box.GrowToInclude(box.bounding_area);
80-
min_x_size = std::min(min_x_size, box.x_size);
81-
min_y_size = std::min(min_y_size, box.y_size);
82-
}
83-
DCHECK_GT(min_x_size, 0);
84-
DCHECK_GT(min_y_size, 0);
85-
86-
// Fixed items are only useful to constraint where the non-fixed items can be
87-
// placed. This means in particular that any part of a fixed item outside the
88-
// bounding box of the non-fixed items is useless. Clip them.
89-
int new_size = 0;
90-
while (new_size < fixed_boxes->size()) {
91-
Rectangle& rectangle = (*fixed_boxes)[new_size];
92-
DCHECK_GT(rectangle.SizeX(), 0);
93-
DCHECK_GT(rectangle.SizeY(), 0);
94-
if (rectangle.x_min < bounding_box.x_min) {
95-
rectangle.x_min = bounding_box.x_min;
96-
changed = true;
97-
}
98-
if (rectangle.x_max > bounding_box.x_max) {
99-
rectangle.x_max = bounding_box.x_max;
100-
changed = true;
101-
}
102-
if (rectangle.y_min < bounding_box.y_min) {
103-
rectangle.y_min = bounding_box.y_min;
104-
changed = true;
105-
}
106-
if (rectangle.y_max > bounding_box.y_max) {
107-
rectangle.y_max = bounding_box.y_max;
108-
changed = true;
109-
}
110-
if (rectangle.SizeX() <= 0 || rectangle.SizeY() <= 0) {
111-
// The whole rectangle was outside of the domain, remove it.
112-
std::swap(rectangle, (*fixed_boxes)[fixed_boxes->size() - 1]);
113-
fixed_boxes->resize(fixed_boxes->size() - 1);
114-
changed = true;
115-
continue;
116-
} else {
117-
new_size++;
118-
}
119-
}
120-
121-
std::vector<Rectangle> optional_boxes = *fixed_boxes;
47+
const Rectangle& bounding_box, IntegerValue min_x_size,
48+
IntegerValue min_y_size) {
49+
std::vector<Rectangle> optional_boxes = {fixed_boxes.begin(),
50+
fixed_boxes.end()};
12251

12352
if (bounding_box.x_min > std::numeric_limits<IntegerValue>::min() &&
12453
bounding_box.y_min > std::numeric_limits<IntegerValue>::min() &&
@@ -228,6 +157,91 @@ bool PresolveFixed2dRectangles(
228157
}
229158
optional_boxes.erase(optional_boxes.begin(),
230159
optional_boxes.begin() + num_optional_boxes_to_remove);
160+
return optional_boxes;
161+
}
162+
163+
} // namespace
164+
165+
bool PresolveFixed2dRectangles(
166+
absl::Span<const RectangleInRange> non_fixed_boxes,
167+
std::vector<Rectangle>* fixed_boxes) {
168+
// This implementation compiles a set of areas that cannot be occupied by any
169+
// item, then calls ReduceNumberofBoxes() to use these areas to minimize
170+
// `fixed_boxes`.
171+
bool changed = false;
172+
173+
DCHECK(FindPartialRectangleIntersections(*fixed_boxes).empty());
174+
IntegerValue original_area = 0;
175+
std::vector<Rectangle> fixed_boxes_copy;
176+
if (VLOG_IS_ON(1)) {
177+
for (const Rectangle& r : *fixed_boxes) {
178+
original_area += r.Area();
179+
}
180+
}
181+
if (VLOG_IS_ON(2)) {
182+
fixed_boxes_copy = *fixed_boxes;
183+
}
184+
185+
const int original_num_boxes = fixed_boxes->size();
186+
187+
// The greedy algorithm is really fast. Run it first since it might greatly
188+
// reduce the size of large trivial instances.
189+
std::vector<Rectangle> empty_vec;
190+
if (ReduceNumberofBoxesGreedy(fixed_boxes, &empty_vec)) {
191+
changed = true;
192+
}
193+
194+
IntegerValue min_x_size = std::numeric_limits<IntegerValue>::max();
195+
IntegerValue min_y_size = std::numeric_limits<IntegerValue>::max();
196+
197+
CHECK(!non_fixed_boxes.empty());
198+
Rectangle bounding_box = non_fixed_boxes[0].bounding_area;
199+
200+
for (const RectangleInRange& box : non_fixed_boxes) {
201+
bounding_box.GrowToInclude(box.bounding_area);
202+
min_x_size = std::min(min_x_size, box.x_size);
203+
min_y_size = std::min(min_y_size, box.y_size);
204+
}
205+
DCHECK_GT(min_x_size, 0);
206+
DCHECK_GT(min_y_size, 0);
207+
208+
// Fixed items are only useful to constraint where the non-fixed items can be
209+
// placed. This means in particular that any part of a fixed item outside the
210+
// bounding box of the non-fixed items is useless. Clip them.
211+
int new_size = 0;
212+
while (new_size < fixed_boxes->size()) {
213+
Rectangle& rectangle = (*fixed_boxes)[new_size];
214+
DCHECK_GT(rectangle.SizeX(), 0);
215+
DCHECK_GT(rectangle.SizeY(), 0);
216+
if (rectangle.x_min < bounding_box.x_min) {
217+
rectangle.x_min = bounding_box.x_min;
218+
changed = true;
219+
}
220+
if (rectangle.x_max > bounding_box.x_max) {
221+
rectangle.x_max = bounding_box.x_max;
222+
changed = true;
223+
}
224+
if (rectangle.y_min < bounding_box.y_min) {
225+
rectangle.y_min = bounding_box.y_min;
226+
changed = true;
227+
}
228+
if (rectangle.y_max > bounding_box.y_max) {
229+
rectangle.y_max = bounding_box.y_max;
230+
changed = true;
231+
}
232+
if (rectangle.SizeX() <= 0 || rectangle.SizeY() <= 0) {
233+
// The whole rectangle was outside of the domain, remove it.
234+
std::swap(rectangle, (*fixed_boxes)[fixed_boxes->size() - 1]);
235+
fixed_boxes->resize(fixed_boxes->size() - 1);
236+
changed = true;
237+
continue;
238+
} else {
239+
new_size++;
240+
}
241+
}
242+
243+
std::vector<Rectangle> optional_boxes = FindSpacesThatCannotBeOccupied(
244+
*fixed_boxes, non_fixed_boxes, bounding_box, min_x_size, min_y_size);
231245

232246
// TODO(user): instead of doing the greedy algorithm first with optional
233247
// boxes, and then the one that is exact for mandatory boxes but weak for
@@ -1467,5 +1481,85 @@ bool ReduceNumberOfBoxesExactMandatory(
14671481
return true;
14681482
}
14691483

1484+
Disjoint2dPackingResult DetectDisjointRegionIn2dPacking(
1485+
absl::Span<const RectangleInRange> non_fixed_boxes,
1486+
absl::Span<const Rectangle> fixed_boxes, int max_num_components) {
1487+
if (max_num_components <= 1) return {};
1488+
1489+
IntegerValue min_x_size = std::numeric_limits<IntegerValue>::max();
1490+
IntegerValue min_y_size = std::numeric_limits<IntegerValue>::max();
1491+
1492+
CHECK(!non_fixed_boxes.empty());
1493+
Rectangle bounding_box = non_fixed_boxes[0].bounding_area;
1494+
1495+
for (const RectangleInRange& box : non_fixed_boxes) {
1496+
bounding_box.GrowToInclude(box.bounding_area);
1497+
min_x_size = std::min(min_x_size, box.x_size);
1498+
min_y_size = std::min(min_y_size, box.y_size);
1499+
}
1500+
DCHECK_GT(min_x_size, 0);
1501+
DCHECK_GT(min_y_size, 0);
1502+
1503+
std::vector<Rectangle> optional_boxes = FindSpacesThatCannotBeOccupied(
1504+
fixed_boxes, non_fixed_boxes, bounding_box, min_x_size, min_y_size);
1505+
std::vector<Rectangle> unoccupiable_space = {fixed_boxes.begin(),
1506+
fixed_boxes.end()};
1507+
unoccupiable_space.insert(unoccupiable_space.end(), optional_boxes.begin(),
1508+
optional_boxes.end());
1509+
1510+
std::vector<Rectangle> occupiable_space =
1511+
FindEmptySpaces(bounding_box, unoccupiable_space);
1512+
1513+
std::vector<Rectangle> empty;
1514+
ReduceNumberofBoxesGreedy(&occupiable_space, &empty);
1515+
std::vector<std::vector<int>> space_components =
1516+
SplitInConnectedComponents(BuildNeighboursGraph(occupiable_space));
1517+
1518+
if (space_components.size() == 1 ||
1519+
space_components.size() > max_num_components) {
1520+
return {};
1521+
}
1522+
1523+
// If we are here, that means that the space where boxes can be placed is not
1524+
// connected.
1525+
Disjoint2dPackingResult result;
1526+
absl::flat_hash_set<int> component_set;
1527+
for (const std::vector<int>& component : space_components) {
1528+
Rectangle bin_bounding_box = occupiable_space[component[0]];
1529+
for (int i = 1; i < component.size(); ++i) {
1530+
bin_bounding_box.GrowToInclude(occupiable_space[component[i]]);
1531+
}
1532+
std::optional<Rectangle> reachable_area_bounding_box;
1533+
Disjoint2dPackingResult::Bin& bin = result.bins.emplace_back();
1534+
for (int idx : component) {
1535+
bin.bin_area.push_back(occupiable_space[idx]);
1536+
}
1537+
for (int i = 0; i < non_fixed_boxes.size(); i++) {
1538+
if (!non_fixed_boxes[i].bounding_area.IsDisjoint(bin_bounding_box)) {
1539+
if (reachable_area_bounding_box.has_value()) {
1540+
reachable_area_bounding_box->GrowToInclude(
1541+
non_fixed_boxes[i].bounding_area);
1542+
} else {
1543+
reachable_area_bounding_box = non_fixed_boxes[i].bounding_area;
1544+
}
1545+
bin.non_fixed_box_indexes.push_back(i);
1546+
}
1547+
}
1548+
if (bin.non_fixed_box_indexes.empty()) {
1549+
result.bins.pop_back();
1550+
continue;
1551+
}
1552+
bin.fixed_boxes =
1553+
FindEmptySpaces(*reachable_area_bounding_box, bin.bin_area);
1554+
ReduceNumberofBoxesGreedy(&bin.fixed_boxes, &empty);
1555+
}
1556+
VLOG_EVERY_N_SEC(1, 1) << "Detected a bin packing problem with "
1557+
<< result.bins.size()
1558+
<< " bins. Original problem sizes: "
1559+
<< non_fixed_boxes.size() << " non-fixed boxes, "
1560+
<< fixed_boxes.size() << " fixed boxes.";
1561+
return result;
1562+
}
1563+
14701564
} // namespace sat
14711565
} // namespace operations_research

ortools/sat/2d_rectangle_presolve.h

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,70 @@ bool PresolveFixed2dRectangles(
3838
absl::Span<const RectangleInRange> non_fixed_boxes,
3939
std::vector<Rectangle>* fixed_boxes);
4040

41+
// Detect whether the fixed boxes of a no_overlap_2d constraint are splitting
42+
// the space into separate components and thus can be replaced by one
43+
// no_overlap_2d constraint per component. If this is not possible, return an
44+
// empty result. Otherwise, return a struct containing what boxes (fixed and
45+
// non-fixed) are needed in each new constraint.
46+
//
47+
// Note that for this to be correct, we need to introduce new boxes to "fill"
48+
// the space occupied by the other components. For example, if we have a
49+
// no_overlap_2d constraint with the fixed boxes and the bounding box of the
50+
// non-fixed boxes as follows:
51+
// +---------------------+
52+
// | |
53+
// | |
54+
// | |
55+
// | |
56+
// | |
57+
// | ++++++++++++++++++|
58+
// | ++++++++++++++++++|
59+
// | ++++++++++++++++++|
60+
// |***** |
61+
// |***** |
62+
// |***** |
63+
// | |
64+
// | |
65+
// | |
66+
// +---------------------+
67+
// and we are building the new no_overlap_2d constraint for the space below, we
68+
// would need to add two new fixed boxes (the '.' and the 'o' one):
69+
// +---------------------+
70+
// |ooooooooooooooooooooo|
71+
// |ooooooooooooooooooooo|
72+
// |ooooooooooooooooooooo|
73+
// |ooooooooooooooooooooo|
74+
// |ooooooooooooooooooooo|
75+
// |...++++++++++++++++++|
76+
// |...++++++++++++++++++|
77+
// |...++++++++++++++++++|
78+
// |***** |
79+
// |***** |
80+
// |***** |
81+
// | |
82+
// | |
83+
// | |
84+
// +---------------------+
85+
// This ensures that the new no_overlap_2d constraint will impose the box to be
86+
// in that component as it should. Note that in the example above, the number of
87+
// boxes won't really increase after a presolve pass, since the presolve for
88+
// fixed boxes will simplify it to two fixed boxes again.
89+
struct Disjoint2dPackingResult {
90+
struct Bin {
91+
// Fixed boxes that the non-fixed boxes in this bin cannot overlap with.
92+
std::vector<Rectangle> fixed_boxes;
93+
// Non-fixed boxes on the original problem to copy to this new constraint.
94+
std::vector<int> non_fixed_box_indexes;
95+
// Area that is covered by the connected component bin represents encoded as
96+
// a non-overlapping set of rectangles.
97+
std::vector<Rectangle> bin_area;
98+
};
99+
std::vector<Bin> bins;
100+
};
101+
Disjoint2dPackingResult DetectDisjointRegionIn2dPacking(
102+
absl::Span<const RectangleInRange> non_fixed_boxes,
103+
absl::Span<const Rectangle> fixed_boxes, int max_num_components);
104+
41105
// Given two vectors of non-overlapping rectangles defining two regions of the
42106
// space: one mandatory region that must be occupied and one optional region
43107
// that can be occupied, try to build a vector of as few non-overlapping

0 commit comments

Comments
 (0)