Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
2d_try_edge_propagator.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <cstdint>
17#include <optional>
18#include <string>
19#include <utility>
20#include <vector>
21
22#include "absl/algorithm/container.h"
23#include "absl/log/check.h"
24#include "absl/log/log.h"
25#include "absl/log/vlog_is_on.h"
26#include "absl/types/span.h"
29#include "ortools/sat/integer.h"
31#include "ortools/sat/model.h"
34#include "ortools/sat/util.h"
39
40namespace operations_research {
41namespace sat {
42
44 const int id = watcher->Register(this);
45 helper_.WatchAllBoxes(id);
47 return id;
48}
49
51 if (!VLOG_IS_ON(1)) return;
52 std::vector<std::pair<std::string, int64_t>> stats;
53 stats.push_back({"TryEdgeRectanglePropagator/called", num_calls_});
54 stats.push_back({"TryEdgeRectanglePropagator/conflicts", num_conflicts_});
55 stats.push_back(
56 {"TryEdgeRectanglePropagator/propagations", num_propagations_});
57
58 shared_stats_->AddStats(stats);
59}
60
61void TryEdgeRectanglePropagator::PopulateActiveBoxRanges() {
62 const int num_boxes = helper_.NumBoxes();
63 placed_boxes_.resize(num_boxes);
64 active_box_ranges_.resize(num_boxes);
65 is_active_.resize(num_boxes);
67 mandatory_regions_.resize(num_boxes);
68 is_in_cache_.resize(num_boxes);
69
70 changed_mandatory_.clear();
71 changed_item_.clear();
72 for (int box = 0; box < num_boxes; ++box) {
73 bool inactive = !helper_.IsPresent(box);
75 if (!inactive) {
76 rec = helper_.GetItemRangeForSizeMin(box);
77 if (rec.x_size == 0 || rec.y_size == 0) {
78 inactive = true;
79 }
80 }
81 is_active_[box] = !inactive;
82 if (inactive) {
83 is_in_cache_[box] = false;
84 has_mandatory_region_.Set(box, false);
85 continue;
86 }
87 if (is_in_cache_[box] && rec == active_box_ranges_[box]) {
88 DCHECK(mandatory_regions_[box] == rec.GetMandatoryRegion());
89 DCHECK(has_mandatory_region_[box] ==
91 continue;
92 }
93 active_box_ranges_[box] = rec;
94 changed_item_.push_back(box);
95 const Rectangle mandatory_region = rec.GetMandatoryRegion();
96 const bool has_mandatory_region =
97 (mandatory_region != Rectangle::GetEmpty());
98 if (has_mandatory_region) {
99 if (!has_mandatory_region_[box] || !is_in_cache_[box] ||
100 mandatory_region != mandatory_regions_[box]) {
101 changed_mandatory_.push_back(box);
102 }
103 }
104 mandatory_regions_[box] = mandatory_region;
105 has_mandatory_region_.Set(box, has_mandatory_region);
106 is_in_cache_[box] = false;
107 }
108}
109
110bool TryEdgeRectanglePropagator::CanPlace(
111 int box_index, const std::pair<IntegerValue, IntegerValue>& position,
112 CompactVectorVector<int>* with_conflict) const {
113 bool can_place = true;
114 if (with_conflict != nullptr) {
115 with_conflict->Add({});
116 }
117 const Rectangle placed_box = {
118 .x_min = position.first,
119 .x_max = position.first + active_box_ranges_[box_index].x_size,
120 .y_min = position.second,
121 .y_max = position.second + active_box_ranges_[box_index].y_size};
122 const absl::Span<const Rectangle> mandatory_regions = mandatory_regions_;
123 for (const int i : has_mandatory_region_) {
124 if (i == box_index) continue;
125 const Rectangle& mandatory_region = mandatory_regions[i];
126 if (!mandatory_region.IsDisjoint(placed_box)) {
127 if (with_conflict != nullptr) {
128 with_conflict->AppendToLastVector(i);
129 can_place = false;
130 } else {
131 return false;
132 }
133 }
134 }
135 return can_place;
136}
137
139 if (!helper_.SynchronizeAndSetDirection(
140 x_is_forward_after_swap_, y_is_forward_after_swap_, swap_x_and_y_)) {
141 return false;
142 }
143
144 num_calls_++;
145
146 PopulateActiveBoxRanges();
147
148 // Our algo is quadratic, so we don't want to run it on really large problems.
149 if (changed_item_.size() > 1000) {
150 return true;
151 }
152
153 // If a mandatory region is changed, we need to replace any cached box that
154 // now became overlapping with it.
155 const int num_active_ranges = active_box_ranges_.size();
156 for (const int mandatory_idx : changed_mandatory_) {
157 const Rectangle& mandatory_region = mandatory_regions_[mandatory_idx];
158 for (int i = 0; i < num_active_ranges; i++) {
159 if (i == mandatory_idx || !is_in_cache_[i]) continue;
160 if (!placed_boxes_[i].IsDisjoint(mandatory_region)) {
161 changed_item_.push_back(i);
162 is_in_cache_[i] = false;
163 }
164 }
165 }
166
167 if (changed_item_.empty()) {
168 return true;
169 }
171
172 potential_x_positions_.clear();
173 potential_y_positions_.clear();
174 for (const int i : has_mandatory_region_) {
175 const Rectangle& mandatory_region = mandatory_regions_[i];
176 potential_x_positions_.push_back(mandatory_region.x_max);
177 potential_y_positions_.push_back(mandatory_region.y_max);
178 }
179
180 gtl::STLSortAndRemoveDuplicates(&potential_x_positions_);
181 gtl::STLSortAndRemoveDuplicates(&potential_y_positions_);
182
183 std::vector<std::pair<int, std::optional<IntegerValue>>> found_propagations;
184 for (const int i : changed_item_) {
185 DCHECK(!is_in_cache_[i]);
186 DCHECK(is_active_[i]);
188
189 if (CanPlace(i, {box.bounding_area.x_min, box.bounding_area.y_min})) {
190 placed_boxes_[i] = {.x_min = box.bounding_area.x_min,
191 .x_max = box.bounding_area.x_min + box.x_size,
192 .y_min = box.bounding_area.y_min,
193 .y_max = box.bounding_area.y_min + box.y_size};
194 is_in_cache_[i] = true;
195 continue;
196 }
197
198 bool placed_at_x_min = false;
199 const int y_start =
200 absl::c_lower_bound(potential_y_positions_, box.bounding_area.y_min) -
201 potential_y_positions_.begin();
202 for (int j = y_start; j < potential_y_positions_.size(); ++j) {
203 if (potential_y_positions_[j] > box.bounding_area.y_max - box.y_size) {
204 // potential_y_positions is sorted, so we can stop here.
205 break;
206 }
207 if (CanPlace(i, {box.bounding_area.x_min, potential_y_positions_[j]})) {
208 placed_at_x_min = true;
209 placed_boxes_[i] = {.x_min = box.bounding_area.x_min,
210 .x_max = box.bounding_area.x_min + box.x_size,
211 .y_min = potential_y_positions_[j],
212 .y_max = potential_y_positions_[j] + box.y_size};
213 is_in_cache_[i] = true;
214 break;
215 }
216 }
217 if (placed_at_x_min) continue;
218
219 // We could not find any placement of the box at its current lower bound!
220 // Thus, we are sure we have something to propagate. Let's find the new
221 // lower bound (or a conflict). Note that the code below is much less
222 // performance critical than the code above, since it only triggers on
223 // propagations.
224 std::optional<IntegerValue> new_x_min = std::nullopt;
225 for (int j = 0; j < potential_x_positions_.size(); ++j) {
226 if (potential_x_positions_[j] < box.bounding_area.x_min) {
227 continue;
228 }
229 if (potential_x_positions_[j] > box.bounding_area.x_max - box.x_size) {
230 continue;
231 }
232 if (CanPlace(i, {potential_x_positions_[j], box.bounding_area.y_min})) {
233 new_x_min = potential_x_positions_[j];
234 break;
235 }
236 for (int k = y_start; k < potential_y_positions_.size(); ++k) {
237 const IntegerValue potential_y_position = potential_y_positions_[k];
238 if (potential_y_position > box.bounding_area.y_max - box.y_size) {
239 break;
240 }
241 if (CanPlace(i, {potential_x_positions_[j], potential_y_position})) {
242 // potential_x_positions is sorted, so the first we found is the
243 // lowest one.
244 new_x_min = potential_x_positions_[j];
245 break;
246 }
247 }
248 if (new_x_min.has_value()) {
249 break;
250 }
251 }
252 found_propagations.push_back({i, new_x_min});
253 }
254
255 return ExplainAndPropagate(found_propagations);
256}
257
259 int box_index, IntegerValue new_x_min) {
260 // We know that we can't place the box at x < new_x_min (which can be
261 // start_max for a conflict). The explanation for the propagation is complex:
262 // we tried a lot of positions, and each one overlaps with the mandatory part
263 // of at least one box. We want to find the smallest set of "conflicting
264 // boxes" that would still forbid every possible placement. To do that, we
265 // build a vector with, for each placement position, the list boxes that
266 // conflict when placing the box at that position. Then we solve
267 // (approximately) a set cover problem to find the smallest set of boxes that
268 // will still makes all positions conflicting.
269 const RectangleInRange& box = active_box_ranges_[box_index];
270
271 // We need to rerun the main propagator loop logic, but this time keeping
272 // track of which boxes conflicted for each position.
273 const int y_start =
274 absl::c_lower_bound(potential_y_positions_, box.bounding_area.y_min) -
275 potential_y_positions_.begin();
276 conflicts_per_x_and_y_.clear();
277 CHECK(!CanPlace(box_index, {box.bounding_area.x_min, box.bounding_area.y_min},
278 &conflicts_per_x_and_y_));
279 for (int j = y_start; j < potential_y_positions_.size(); ++j) {
280 if (potential_y_positions_[j] > box.bounding_area.y_max - box.y_size) {
281 // potential_y_positions is sorted, so we can stop here.
282 break;
283 }
284 CHECK(!CanPlace(box_index,
285 {box.bounding_area.x_min, potential_y_positions_[j]},
286 &conflicts_per_x_and_y_));
287 }
288 for (int j = 0; j < potential_x_positions_.size(); ++j) {
289 if (potential_x_positions_[j] < box.bounding_area.x_min) {
290 continue;
291 }
292 if (potential_x_positions_[j] >= new_x_min) {
293 continue;
294 }
295 CHECK(!CanPlace(box_index,
296 {potential_x_positions_[j], box.bounding_area.y_min},
297 &conflicts_per_x_and_y_));
298 for (int k = y_start; k < potential_y_positions_.size(); ++k) {
299 const IntegerValue potential_y_position = potential_y_positions_[k];
300 if (potential_y_position > box.bounding_area.y_max - box.y_size) {
301 break;
302 }
303 CHECK(!CanPlace(box_index,
304 {potential_x_positions_[j], potential_y_position},
305 &conflicts_per_x_and_y_));
306 }
307 }
308
309 // Now gather the data per box to make easier to use the set cover solver API.
310 // TODO(user): skip the boxes that are fixed at level zero. They do not
311 // contribute to the size of the explanation (so we shouldn't minimize their
312 // number) and make the SetCover problem harder to solve.
313 std::vector<std::vector<int>> conflicting_position_per_box(
314 active_box_ranges_.size(), std::vector<int>());
315 for (int i = 0; i < conflicts_per_x_and_y_.size(); ++i) {
316 DCHECK(!conflicts_per_x_and_y_[i].empty());
317 for (const int j : conflicts_per_x_and_y_[i]) {
318 conflicting_position_per_box[j].push_back(i);
319 }
320 }
321 SetCoverModel model;
322 for (const auto& conflicts : conflicting_position_per_box) {
323 if (conflicts.empty()) continue;
324 model.AddEmptySubset(/*cost=*/1);
325 for (const int i : conflicts) {
327 }
328 }
329 DCHECK(model.ComputeFeasibility());
330 SetCoverInvariant inv(&model);
331 LazyElementDegreeSolutionGenerator greedy_search(&inv);
332 CHECK(greedy_search.NextSolution());
333 LazySteepestSearch steepest_search(&inv);
334 CHECK(steepest_search.NextSolution());
335 GuidedLocalSearch search(&inv);
336 CHECK(search.SetMaxIterations(100).NextSolution());
337 DCHECK(inv.CheckConsistency(
339
340 int count = 0;
341 const auto& solution = inv.is_selected();
342 std::vector<int> boxes_participating_in_propagation;
343 boxes_participating_in_propagation.reserve(model.num_subsets() + 1);
344 boxes_participating_in_propagation.push_back(box_index);
345 for (int i = 0; i < conflicting_position_per_box.size(); ++i) {
346 const auto& conflicts = conflicting_position_per_box[i];
347 if (conflicts.empty()) continue;
348 if (solution[SubsetIndex(count)]) {
349 boxes_participating_in_propagation.push_back(i);
350 }
351 count++;
352 }
353 VLOG_EVERY_N_SEC(3, 2) << "Found no_overlap_2d constraint propagation with "
354 << boxes_participating_in_propagation.size() << "/"
355 << (model.num_subsets() + 1) << " items";
356
357 // TODO(user): We now know for each box the list of placements that it
358 // contributes to the conflict. We could use this information to relax the
359 // bounds of this box on the explanation of the propagation. For example, for
360 // a box that always overlaps at least five units to the right when it does,
361 // we could call AddStartMinReason(x_min - 4) instead of
362 // AddStartMinReason(x_min).
363 return boxes_participating_in_propagation;
364}
365
367 const std::vector<std::pair<int, std::optional<IntegerValue>>>&
368 found_propagations) {
369 for (const auto& [box_index, new_x_min] : found_propagations) {
370 const RectangleInRange& box = active_box_ranges_[box_index];
371 helper_.ClearReason();
372
373 const std::vector<int> minimum_problem_with_propagator =
375 box_index, new_x_min.has_value()
376 ? *new_x_min
377 : box.bounding_area.x_max - box.x_size);
378 for (const int j : minimum_problem_with_propagator) {
379 DCHECK(is_active_[j]);
380 // Important: we also add to the reason the actual box we are changing the
381 // x_min. This is important, since we don't check if there are any
382 // feasible placement before its current x_min, so it needs to be part of
383 // the reason.
384 const RectangleInRange& box_reason = active_box_ranges_[j];
385 const int b = box_reason.box_index;
386
387 helper_.AddLeftMinReason(b, box_reason.bounding_area.x_min);
388 helper_.AddBottomMinReason(b, box_reason.bounding_area.y_min);
389
390 if (j != box_index || !new_x_min.has_value()) {
391 // We don't need to add to the reason the x_max for the box we are
392 // pushing the x_min, except if we found a conflict.
393 helper_.AddLeftMaxReason(
394 b, box_reason.bounding_area.x_max - box_reason.x_size);
395 }
396 helper_.AddBottomMaxReason(
397 b, box_reason.bounding_area.y_max - box_reason.y_size);
398
399 helper_.AddSizeMinReason(b);
400 helper_.AddPresenceReason(b);
401 }
402 if (new_x_min.has_value()) {
403 num_propagations_++;
404 if (!helper_.IncreaseLeftMin(box_index, *new_x_min)) {
405 return false;
406 }
407 } else {
408 num_conflicts_++;
409 return helper_.ReportConflict();
410 }
411 }
412 return true;
413}
414
416 Model* model,
417 GenericLiteralWatcher* watcher,
418 int priority) {
419 TryEdgeRectanglePropagator* try_edge_propagator =
420 new TryEdgeRectanglePropagator(true, true, false, helper, model);
421 watcher->SetPropagatorPriority(try_edge_propagator->RegisterWith(watcher),
422 priority);
423 model->TakeOwnership(try_edge_propagator);
424
425 TryEdgeRectanglePropagator* try_edge_propagator_mirrored =
426 new TryEdgeRectanglePropagator(false, true, false, helper, model);
427 watcher->SetPropagatorPriority(
428 try_edge_propagator_mirrored->RegisterWith(watcher), priority);
429 model->TakeOwnership(try_edge_propagator_mirrored);
430
431 TryEdgeRectanglePropagator* try_edge_propagator_swap =
432 new TryEdgeRectanglePropagator(true, true, true, helper, model);
433 watcher->SetPropagatorPriority(
434 try_edge_propagator_swap->RegisterWith(watcher), priority);
435 model->TakeOwnership(try_edge_propagator_swap);
436
437 TryEdgeRectanglePropagator* try_edge_propagator_swap_mirrored =
438 new TryEdgeRectanglePropagator(false, true, true, helper, model);
439 watcher->SetPropagatorPriority(
440 try_edge_propagator_swap_mirrored->RegisterWith(watcher), priority);
441 model->TakeOwnership(try_edge_propagator_swap_mirrored);
442}
443
444} // namespace sat
445} // namespace operations_research
void resize(int size)
Resizes the Bitset64 to the given number of bits. New bits are sets to 0.
Definition bitset.h:473
void Set(IndexType i)
Sets the bit at position i to 1.
Definition bitset.h:543
bool NextSolution(const SubsetBoolVector &in_focus) final
bool NextSolution(const SubsetBoolVector &in_focus) final
LazySteepestSearch.
bool CheckConsistency(ConsistencyLevel consistency) const
Checks the consistency of the invariant at the given consistency level.
const SubsetBoolVector & is_selected() const
Returns the subset assignment vector.
Main class for describing a weighted set-covering problem.
void AddElementToLastSubset(BaseInt element)
void SetPropagatorPriority(int id, int priority)
Definition integer.cc:2352
int Register(PropagatorInterface *propagator)
Registers a propagator and returns its unique ids.
Definition integer.cc:2326
std::vector< int > GetMinimumProblemWithPropagation(int box_index, IntegerValue new_x_min)
virtual bool ExplainAndPropagate(const std::vector< std::pair< int, std::optional< IntegerValue > > > &found_propagations)
void STLSortAndRemoveDuplicates(T *v, const LessFunc &less_func)
Definition stl_util.h:55
void CreateAndRegisterTryEdgePropagator(NoOverlap2DConstraintHelper *helper, Model *model, GenericLiteralWatcher *watcher, int priority)
In SWIG mode, we don't want anything besides these top-level includes.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid