Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
routing_cuts.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 <algorithm>
17#include <cmath>
18#include <cstdint>
19#include <cstdlib>
20#include <functional>
21#include <memory>
22#include <numeric>
23#include <string>
24#include <tuple>
25#include <utility>
26#include <vector>
27
28#include "absl/algorithm/container.h"
29#include "absl/cleanup/cleanup.h"
30#include "absl/container/flat_hash_map.h"
31#include "absl/container/flat_hash_set.h"
32#include "absl/log/check.h"
33#include "absl/random/distributions.h"
34#include "absl/strings/str_cat.h"
35#include "absl/types/span.h"
39#include "ortools/graph/graph.h"
41#include "ortools/sat/cuts.h"
42#include "ortools/sat/integer.h"
46#include "ortools/sat/model.h"
49#include "ortools/sat/util.h"
51
52namespace operations_research {
53namespace sat {
54
55namespace {
56
57// If we have many sets of (var >= values relations) and we know at least one
58// set is true, then we can derive valid global lower bounds on the variable
59// that appear in all such sets.
60//
61// This represents "one step" of computing such bounds by adding the sets one at
62// the time. When we process a set 'new_lbs':
63// - For the first set, then 'new_lbs' is globally valid.
64// - For a new set, we need to erase variables not appearing in new_lbs and
65// take the min for the ones appearing in both.
66//
67// TODO(user): The operation is symmetric, so we could swap both hash_map if
68// new_lbs is smaller than current_lbs as a small optimization.
69void ComputeMinLowerBoundOfSharedVariables(
70 const absl::flat_hash_map<IntegerVariable, IntegerValue>& new_lbs,
71 absl::flat_hash_map<IntegerVariable, IntegerValue>* current_lbs) {
72 if (new_lbs.empty()) current_lbs->clear();
73 for (auto it = current_lbs->begin(); it != current_lbs->end();) {
74 const IntegerVariable var = it->first;
75 auto other_it = new_lbs.find(var);
76 if (other_it == new_lbs.end()) {
77 // We have no bounds about var in the new_fact, we need to erase it.
78 //
79 // Note: it = current_lbs->erase(it) does not work for flat_hash_map, this
80 // is the recommended way.
81 current_lbs->erase(it++);
82 } else {
83 // Update.
84 it->second = std::min(it->second, other_it->second);
85 ++it;
86 }
87 }
88}
89
90} // namespace
91
93 int num_nodes, const std::vector<int>& tails, const std::vector<int>& heads,
94 const std::vector<Literal>& literals, Model* model)
95 : tails_(tails),
96 heads_(heads),
97 literals_(literals),
98 binary_relation_repository_(
99 *model->GetOrCreate<BinaryRelationRepository>()),
100 trail_(*model->GetOrCreate<Trail>()),
101 integer_trail_(*model->GetOrCreate<IntegerTrail>()),
102 in_subset_(num_nodes, false),
103 index_in_subset_(num_nodes, -1),
104 incoming_arc_indices_(num_nodes),
105 outgoing_arc_indices_(num_nodes),
106 reachable_(num_nodes, false),
107 next_reachable_(num_nodes, false),
108 node_var_lower_bounds_(num_nodes),
109 next_node_var_lower_bounds_(num_nodes) {}
110
112 absl::Span<const int> subset) {
113 DCHECK_GE(subset.size(), 1);
114 DCHECK(absl::c_all_of(in_subset_, [](bool b) { return !b; }));
115 DCHECK(absl::c_all_of(incoming_arc_indices_,
116 [](const auto& v) { return v.empty(); }));
117 DCHECK(absl::c_all_of(reachable_, [](bool b) { return !b; }));
118 DCHECK(absl::c_all_of(next_reachable_, [](bool b) { return !b; }));
119 DCHECK(absl::c_all_of(node_var_lower_bounds_,
120 [](const auto& m) { return m.empty(); }));
121 DCHECK(absl::c_all_of(next_node_var_lower_bounds_,
122 [](const auto& m) { return m.empty(); }));
123
124 for (const int n : subset) {
125 in_subset_[n] = true;
126 // Conservatively assume that each subset node is reachable from outside.
127 reachable_[n] = true;
128 }
129 const int num_arcs = tails_.size();
130 for (int i = 0; i < num_arcs; ++i) {
131 if (in_subset_[tails_[i]] && in_subset_[heads_[i]] &&
132 heads_[i] != tails_[i]) {
133 incoming_arc_indices_[heads_[i]].push_back(i);
134 }
135 }
136
137 // Maximum number of nodes of a feasible path inside the subset.
138 int longest_path_length = 1;
139 absl::flat_hash_map<IntegerVariable, IntegerValue> tmp_lbs;
140 while (longest_path_length < subset.size()) {
141 bool at_least_one_next_node_reachable = false;
142 for (const int head : subset) {
143 bool is_reachable = false;
144 for (const int incoming_arc_index : incoming_arc_indices_[head]) {
145 const int tail = tails_[incoming_arc_index];
146 const Literal lit = literals_[incoming_arc_index];
147 if (!reachable_[tail]) continue;
148
149 // If this arc cannot be taken skip.
150 tmp_lbs.clear();
151 if (!binary_relation_repository_.PropagateLocalBounds(
152 integer_trail_, lit, node_var_lower_bounds_[tail], &tmp_lbs)) {
153 continue;
154 }
155
156 if (!is_reachable) {
157 // This is the first arc that reach this node.
158 is_reachable = true;
159 next_node_var_lower_bounds_[head] = tmp_lbs;
160 } else {
161 // Combine information with previous one.
162 ComputeMinLowerBoundOfSharedVariables(
163 tmp_lbs, &next_node_var_lower_bounds_[head]);
164 }
165 }
166
167 next_reachable_[head] = is_reachable;
168 if (next_reachable_[head]) at_least_one_next_node_reachable = true;
169 }
170 if (!at_least_one_next_node_reachable) {
171 break;
172 }
173 std::swap(reachable_, next_reachable_);
174 std::swap(node_var_lower_bounds_, next_node_var_lower_bounds_);
175 for (const int n : subset) {
176 next_node_var_lower_bounds_[n].clear();
177 }
178 ++longest_path_length;
179 }
180
181 // The maximum number of distinct paths of length `longest_path_length`.
182 int max_longest_paths = 0;
183 // Reset the temporary data structures for the next call.
184 for (const int n : subset) {
185 in_subset_[n] = false;
186 incoming_arc_indices_[n].clear();
187 if (reachable_[n]) ++max_longest_paths;
188 reachable_[n] = false;
189 next_reachable_[n] = false;
190 node_var_lower_bounds_[n].clear();
191 next_node_var_lower_bounds_[n].clear();
192 }
193 return GetMinOutgoingFlow(subset.size(), longest_path_length,
194 max_longest_paths);
195}
196
197int MinOutgoingFlowHelper::GetMinOutgoingFlow(int subset_size,
198 int longest_path_length,
199 int max_longest_paths) {
200 if (max_longest_paths * longest_path_length < subset_size) {
201 // If `longest_path_length` is 1, there should be one such path per node,
202 // and thus `max_longest_paths` should be the subset size. This branch can
203 // thus not be taken in this case.
204 DCHECK_GT(longest_path_length, 1);
205 // TODO(user): Consider using information about the number of shorter
206 // paths to derive an even better bound.
207 return max_longest_paths +
209 subset_size - max_longest_paths * longest_path_length,
210 longest_path_length - 1);
211 }
212 return MathUtil::CeilOfRatio(subset_size, longest_path_length);
213}
214
215namespace {
216struct Path {
217 uint32_t node_set; // Bit i is set iif node subset[i] is in the path.
218 int last_node; // The last node in the path.
219
220 bool operator==(const Path& p) const {
221 return node_set == p.node_set && last_node == p.last_node;
222 }
223
224 template <typename H>
225 friend H AbslHashValue(H h, const Path& p) {
226 return H::combine(std::move(h), p.node_set, p.last_node);
227 }
228};
229
230struct PathVariableBounds {
231 absl::flat_hash_set<int> incoming_arc_indices;
232 absl::flat_hash_map<IntegerVariable, absl::flat_hash_map<int, IntegerValue>>
233 lower_bound_by_var_and_arc_index;
234};
235} // namespace
236
238 absl::Span<const int> subset) {
239 DCHECK_GE(subset.size(), 1);
240 DCHECK_LE(subset.size(), 32);
241 DCHECK(absl::c_all_of(index_in_subset_, [](int i) { return i == -1; }));
242 DCHECK(absl::c_all_of(outgoing_arc_indices_,
243 [](const auto& v) { return v.empty(); }));
244
245 std::vector<int> longest_path_length_by_end_node(subset.size(), 1);
246 for (int i = 0; i < subset.size(); ++i) {
247 index_in_subset_[subset[i]] = i;
248 }
249 for (int i = 0; i < tails_.size(); ++i) {
250 if (index_in_subset_[tails_[i]] != -1 &&
251 index_in_subset_[heads_[i]] != -1 && heads_[i] != tails_[i]) {
252 outgoing_arc_indices_[tails_[i]].push_back(i);
253 }
254 }
255
256 absl::flat_hash_map<Path, PathVariableBounds> path_var_bounds;
257 std::vector<Path> paths;
258 std::vector<Path> next_paths;
259 for (int i = 0; i < subset.size(); ++i) {
260 paths.push_back(
261 {.node_set = static_cast<uint32_t>(1 << i), .last_node = subset[i]});
262 }
263 int longest_path_length = 1;
264 for (int path_length = 1; path_length <= subset.size(); ++path_length) {
265 for (const Path& path : paths) {
266 // Merge the bounds by variable and arc incoming to the last node of the
267 // path into bounds by variable, if possible, and check whether they are
268 // feasible or not.
269 const auto& var_bounds = path_var_bounds[path];
270 absl::flat_hash_map<IntegerVariable, IntegerValue> lower_bound_by_var;
271 for (const auto& [var, lower_bound_by_arc_index] :
272 var_bounds.lower_bound_by_var_and_arc_index) {
273 // If each arc which can reach the last node of the path enforces some
274 // lower bound for `var`, then the lower bound of `var` can be increased
275 // to the minimum of these arc-specific lower bounds (since at least one
276 // of these arcs must be selected to reach this node).
277 if (lower_bound_by_arc_index.size() !=
278 var_bounds.incoming_arc_indices.size()) {
279 continue;
280 }
281 IntegerValue lb = lower_bound_by_arc_index.begin()->second;
282 for (const auto& [_, lower_bound] : lower_bound_by_arc_index) {
283 lb = std::min(lb, lower_bound);
284 }
285 if (lb > integer_trail_.LevelZeroLowerBound(var)) {
286 lower_bound_by_var[var] = lb;
287 }
288 }
289 path_var_bounds.erase(path);
290 auto get_lower_bound = [&](IntegerVariable var) {
291 const auto it = lower_bound_by_var.find(var);
292 if (it != lower_bound_by_var.end()) return it->second;
293 return integer_trail_.LevelZeroLowerBound(var);
294 };
295 auto get_upper_bound = [&](IntegerVariable var) {
296 return -get_lower_bound(NegationOf(var));
297 };
298 bool feasible_path = true;
299 for (const auto& [var, lb] : lower_bound_by_var) {
300 if (get_upper_bound(var) < lb) {
301 feasible_path = false;
302 break;
303 }
304 }
305 if (!feasible_path) continue;
306 // We have found a feasible path, update the path length statistics...
307 longest_path_length = path_length;
308 longest_path_length_by_end_node[index_in_subset_[path.last_node]] =
309 path_length;
310
311 // ... and try to extend the path with each arc going out of its last
312 // node.
313 for (const int outgoing_arc_index :
314 outgoing_arc_indices_[path.last_node]) {
315 const int head = heads_[outgoing_arc_index];
316 const int head_index_in_subset = index_in_subset_[head];
317 DCHECK_NE(head_index_in_subset, -1);
318 if (path.node_set & (1 << head_index_in_subset)) continue;
319 const Path new_path = {
320 .node_set = path.node_set | (1 << head_index_in_subset),
321 .last_node = head};
322 if (!path_var_bounds.contains(new_path)) {
323 next_paths.push_back(new_path);
324 }
325 auto& new_var_bounds = path_var_bounds[new_path];
326 new_var_bounds.incoming_arc_indices.insert(outgoing_arc_index);
327 auto update_lower_bound_by_var_and_arc_index =
328 [&](IntegerVariable var, int arc_index, IntegerValue lb) {
329 auto& lower_bound_by_arc_index =
330 new_var_bounds.lower_bound_by_var_and_arc_index[var];
331 auto it = lower_bound_by_arc_index.find(arc_index);
332 if (it != lower_bound_by_arc_index.end()) {
333 it->second = std::max(it->second, lb);
334 } else {
335 lower_bound_by_arc_index[arc_index] = lb;
336 }
337 };
338 auto update_upper_bound_by_var_and_arc_index =
339 [&](IntegerVariable var, int arc_index, IntegerValue ub) {
340 update_lower_bound_by_var_and_arc_index(NegationOf(var),
341 arc_index, -ub);
342 };
343 auto update_var_bounds = [&](int arc_index, LinearTerm a, LinearTerm b,
344 IntegerValue lhs, IntegerValue rhs) {
345 if (a.coeff == 0) return;
346 a.MakeCoeffPositive();
347 b.MakeCoeffPositive();
348 // lb(b.y) <= b.y <= ub(b.y) and lhs <= a.x + b.y <= rhs imply
349 // ceil((lhs - ub(b.y)) / a) <= x <= floor((rhs - lb(b.y)) / a)
350 lhs = lhs - b.coeff * get_upper_bound(b.var);
351 rhs = rhs - b.coeff * get_lower_bound(b.var);
352 update_lower_bound_by_var_and_arc_index(
353 a.var, arc_index, MathUtil::CeilOfRatio(lhs, a.coeff));
354 update_upper_bound_by_var_and_arc_index(
355 a.var, arc_index, MathUtil::FloorOfRatio(rhs, a.coeff));
356 };
357 const Literal lit = literals_[outgoing_arc_index];
358 for (const int relation_index :
359 binary_relation_repository_.relation_indices(lit)) {
360 const auto& r = binary_relation_repository_.relation(relation_index);
361 update_var_bounds(outgoing_arc_index, r.a, r.b, r.lhs, r.rhs);
362 update_var_bounds(outgoing_arc_index, r.b, r.a, r.lhs, r.rhs);
363 }
364 }
365 }
366 std::swap(paths, next_paths);
367 next_paths.clear();
368 }
369
370 int max_longest_paths = 0;
371 for (int i = 0; i < subset.size(); ++i) {
372 if (longest_path_length_by_end_node[i] == longest_path_length) {
373 ++max_longest_paths;
374 }
375 }
376 // Reset the temporary data structures for the next call.
377 for (const int n : subset) {
378 index_in_subset_[n] = -1;
379 outgoing_arc_indices_[n].clear();
380 }
381 return GetMinOutgoingFlow(subset.size(), longest_path_length,
382 max_longest_paths);
383}
384
385namespace {
386
387class OutgoingCutHelper {
388 public:
389 OutgoingCutHelper(int num_nodes, bool is_route_constraint, int64_t capacity,
390 absl::Span<const int64_t> demands,
391 absl::Span<const int> tails, absl::Span<const int> heads,
392 absl::Span<const Literal> literals, Model* model)
393 : num_nodes_(num_nodes),
394 is_route_constraint_(is_route_constraint),
395 capacity_(capacity),
396 demands_(demands.begin(), demands.end()),
397 tails_(tails.begin(), tails.end()),
398 heads_(heads.begin(), heads.end()),
399 literals_(literals.begin(), literals.end()),
400 literal_lp_values_(literals.size()),
401 params_(*model->GetOrCreate<SatParameters>()),
402 trail_(*model->GetOrCreate<Trail>()),
403 random_(model->GetOrCreate<ModelRandomGenerator>()),
404 encoder_(model->GetOrCreate<IntegerEncoder>()),
405 in_subset_(num_nodes, false),
406 min_outgoing_flow_helper_(num_nodes, tails_, heads_, literals_, model) {
407 // Compute the total demands in order to know the minimum incoming/outgoing
408 // flow.
409 for (const int64_t demand : demands) total_demand_ += demand;
410 complement_of_subset_.reserve(num_nodes_);
411 }
412
413 int num_nodes() const { return num_nodes_; }
414
415 bool is_route_constraint() const { return is_route_constraint_; }
416
417 // Returns the arcs computed by InitializeForNewLpSolution().
418 absl::Span<const ArcWithLpValue> relevant_arcs() const {
419 return relevant_arcs_;
420 }
421
422 // Returns the arcs computed in InitializeForNewLpSolution(), sorted by
423 // decreasing lp_value.
424 absl::Span<const std::pair<int, int>> ordered_arcs() const {
425 return ordered_arcs_;
426 }
427
428 // This should be called each time a new LP solution is available, before
429 // using the other methods.
430 void InitializeForNewLpSolution(LinearConstraintManager* manager);
431
432 // Merge the relevant arcs (tail, head) and (head, tail) into a single (tail,
433 // head) arc, with the sum of their lp_values.
434 absl::Span<const ArcWithLpValue> SymmetrizedRelevantArcs() {
435 symmetrized_relevant_arcs_ = relevant_arcs_;
436 SymmetrizeArcs(&symmetrized_relevant_arcs_);
437 return symmetrized_relevant_arcs_;
438 }
439
440 // Try to add an outgoing cut from the given subset.
441 bool TrySubsetCut(std::string name, absl::Span<const int> subset,
442 LinearConstraintManager* manager);
443
444 // If we look at the symmetrized version (tail <-> head = tail->head +
445 // head->tail) and we split all the edges between a subset of nodes S and the
446 // outside into a set A and the other d(S)\A, and |A| is odd, we have a
447 // constraint of the form:
448 // "all edge of A at 1" => sum other edges >= 1.
449 // This is because a cycle or multiple-cycle must go in/out an even number
450 // of time. This enforced constraint simply linearize to:
451 // sum_d(S)\A x_e + sum_A (1 - x_e) >= 1.
452 //
453 // Given a subset of nodes, it is easy to identify the best subset A of edge
454 // to consider.
455 bool TryBlossomSubsetCut(std::string name,
456 absl::Span<const ArcWithLpValue> symmetrized_edges,
457 absl::Span<const int> subset,
458 LinearConstraintManager* manager);
459
460 private:
461 // Removes the arcs with a literal fixed at false at level zero. This is
462 // especially useful to remove fixed self loop.
463 void FilterFalseArcsAtLevelZero();
464
465 // Add a cut of the form Sum_{outgoing arcs from S} lp >= rhs_lower_bound.
466 //
467 // Note that we used to also add the same cut for the incoming arcs, but
468 // because of flow conservation on these problems, the outgoing flow is always
469 // the same as the incoming flow, so adding this extra cut doesn't seem
470 // relevant.
471 bool AddOutgoingCut(LinearConstraintManager* manager, std::string name,
472 int subset_size, const std::vector<bool>& in_subset,
473 int64_t rhs_lower_bound, int ignore_arcs_with_head);
474
475 const int num_nodes_;
476 const bool is_route_constraint_;
477 const int64_t capacity_;
478 std::vector<int64_t> demands_;
479 std::vector<int> tails_;
480 std::vector<int> heads_;
481 std::vector<Literal> literals_;
482 std::vector<double> literal_lp_values_;
483 std::vector<ArcWithLpValue> relevant_arcs_;
484 std::vector<ArcWithLpValue> symmetrized_relevant_arcs_;
485 std::vector<std::pair<int, int>> ordered_arcs_;
486
487 const SatParameters& params_;
488 const Trail& trail_;
489 ModelRandomGenerator* random_;
490 IntegerEncoder* encoder_;
491
492 int64_t total_demand_ = 0;
493 std::vector<bool> in_subset_;
494 std::vector<int> complement_of_subset_;
495
496 MaxBoundedSubsetSum max_bounded_subset_sum_;
497 MaxBoundedSubsetSumExact max_bounded_subset_sum_exact_;
498 MinOutgoingFlowHelper min_outgoing_flow_helper_;
499};
500
501void OutgoingCutHelper::FilterFalseArcsAtLevelZero() {
502 if (trail_.CurrentDecisionLevel() != 0) return;
503
504 int new_size = 0;
505 const int size = static_cast<int>(tails_.size());
506 const VariablesAssignment& assignment = trail_.Assignment();
507 for (int i = 0; i < size; ++i) {
508 if (assignment.LiteralIsFalse(literals_[i])) continue;
509 tails_[new_size] = tails_[i];
510 heads_[new_size] = heads_[i];
511 literals_[new_size] = literals_[i];
512 ++new_size;
513 }
514 if (new_size < size) {
515 tails_.resize(new_size);
516 heads_.resize(new_size);
517 literals_.resize(new_size);
518 literal_lp_values_.resize(new_size);
519 }
520}
521
522void OutgoingCutHelper::InitializeForNewLpSolution(
523 LinearConstraintManager* manager) {
524 FilterFalseArcsAtLevelZero();
525
526 // We will collect only the arcs with a positive lp_values to speed up some
527 // computation below.
528 relevant_arcs_.clear();
529
530 // Sort the arcs by non-increasing lp_values.
531 const auto& lp_values = manager->LpValues();
532 std::vector<std::pair<double, int>> relevant_arc_by_decreasing_lp_values;
533 for (int i = 0; i < literals_.size(); ++i) {
534 double lp_value;
535 const IntegerVariable direct_view = encoder_->GetLiteralView(literals_[i]);
536 if (direct_view != kNoIntegerVariable) {
537 lp_value = lp_values[direct_view];
538 } else {
539 lp_value =
540 1.0 - lp_values[encoder_->GetLiteralView(literals_[i].Negated())];
541 }
542 literal_lp_values_[i] = lp_value;
543
544 if (lp_value < 1e-6) continue;
545 relevant_arcs_.push_back({tails_[i], heads_[i], lp_value});
546 relevant_arc_by_decreasing_lp_values.push_back({lp_value, i});
547 }
548 std::sort(relevant_arc_by_decreasing_lp_values.begin(),
549 relevant_arc_by_decreasing_lp_values.end(),
550 std::greater<std::pair<double, int>>());
551
552 ordered_arcs_.clear();
553 for (const auto& [score, arc] : relevant_arc_by_decreasing_lp_values) {
554 ordered_arcs_.push_back({tails_[arc], heads_[arc]});
555 }
556}
557
558bool OutgoingCutHelper::AddOutgoingCut(LinearConstraintManager* manager,
559 std::string name, int subset_size,
560 const std::vector<bool>& in_subset,
561 int64_t rhs_lower_bound,
562 int ignore_arcs_with_head) {
563 // A node is said to be optional if it can be excluded from the subcircuit,
564 // in which case there is a self-loop on that node.
565 // If there are optional nodes, use extended formula:
566 // sum(cut) >= 1 - optional_loop_in - optional_loop_out
567 // where optional_loop_in's node is in subset, optional_loop_out's is out.
568 // TODO(user): Favor optional loops fixed to zero at root.
569 int num_optional_nodes_in = 0;
570 int num_optional_nodes_out = 0;
571 int optional_loop_in = -1;
572 int optional_loop_out = -1;
573 for (int i = 0; i < tails_.size(); ++i) {
574 if (tails_[i] != heads_[i]) continue;
575 if (in_subset[tails_[i]]) {
576 num_optional_nodes_in++;
577 if (optional_loop_in == -1 ||
578 literal_lp_values_[i] < literal_lp_values_[optional_loop_in]) {
579 optional_loop_in = i;
580 }
581 } else {
582 num_optional_nodes_out++;
583 if (optional_loop_out == -1 ||
584 literal_lp_values_[i] < literal_lp_values_[optional_loop_out]) {
585 optional_loop_out = i;
586 }
587 }
588 }
589
590 // TODO(user): The lower bound for CVRP is computed assuming all nodes must be
591 // served, if it is > 1 we lower it to one in the presence of optional nodes.
592 if (num_optional_nodes_in + num_optional_nodes_out > 0) {
593 CHECK_GE(rhs_lower_bound, 1);
594 rhs_lower_bound = 1;
595 ignore_arcs_with_head = -1;
596 }
597
598 // We create the cut and rely on AddCut() for computing its efficacy and
599 // rejecting it if it is bad.
600 LinearConstraintBuilder outgoing(encoder_, IntegerValue(rhs_lower_bound),
601 kMaxIntegerValue);
602
603 // Add outgoing arcs, compute outgoing flow.
604 for (int i = 0; i < tails_.size(); ++i) {
605 if (in_subset[tails_[i]] && !in_subset[heads_[i]]) {
606 if (heads_[i] == ignore_arcs_with_head) continue;
607 CHECK(outgoing.AddLiteralTerm(literals_[i], IntegerValue(1)));
608 }
609 }
610
611 // Support optional nodes if any.
612 if (num_optional_nodes_in + num_optional_nodes_out > 0) {
613 // When all optionals of one side are excluded in lp solution, no cut.
614 if (num_optional_nodes_in == subset_size &&
615 (optional_loop_in == -1 ||
616 literal_lp_values_[optional_loop_in] > 1.0 - 1e-6)) {
617 return false;
618 }
619 if (num_optional_nodes_out == num_nodes_ - subset_size &&
620 (optional_loop_out == -1 ||
621 literal_lp_values_[optional_loop_out] > 1.0 - 1e-6)) {
622 return false;
623 }
624
625 // There is no mandatory node in subset, add optional_loop_in.
626 if (num_optional_nodes_in == subset_size) {
627 CHECK(outgoing.AddLiteralTerm(literals_[optional_loop_in],
628 IntegerValue(1)));
629 }
630
631 // There is no mandatory node out of subset, add optional_loop_out.
632 if (num_optional_nodes_out == num_nodes_ - subset_size) {
633 CHECK(outgoing.AddLiteralTerm(literals_[optional_loop_out],
634 IntegerValue(1)));
635 }
636 }
637
638 return manager->AddCut(outgoing.Build(), name);
639}
640
641bool OutgoingCutHelper::TrySubsetCut(std::string name,
642 absl::Span<const int> subset,
643 LinearConstraintManager* manager) {
644 DCHECK_GE(subset.size(), 1);
645 DCHECK_LT(subset.size(), num_nodes_);
646
647 // Initialize "in_subset" and contain_depot.
648 bool contain_depot = false;
649 for (const int n : subset) {
650 in_subset_[n] = true;
651 if (n == 0 && is_route_constraint_) {
652 contain_depot = true;
653 }
654 }
655
656 // Compute a lower bound on the outgoing flow.
657 //
658 // TODO(user): This lower bound assume all nodes in subset must be served.
659 // If this is not the case, we are really defensive in AddOutgoingCut().
660 // Improve depending on where the self-loop are.
661 int64_t min_outgoing_flow = 1;
662
663 // Bounds inferred automatically from the enforced binary relation of the
664 // model.
665 //
666 // TODO(user): This is still not as good as the "capacity" bounds below in
667 // some cases. Fix! we should be able to use the same relation to infer the
668 // capacity bounds somehow.
669 const int subset_or_complement_size =
670 contain_depot ? num_nodes_ - subset.size() : subset.size();
671 if (subset_or_complement_size <=
672 params_.routing_cut_subset_size_for_binary_relation_bound() &&
673 subset_or_complement_size >=
674 params_.routing_cut_subset_size_for_tight_binary_relation_bound()) {
675 int bound;
676 if (contain_depot) {
677 complement_of_subset_.clear();
678 for (int i = 0; i < num_nodes_; ++i) {
679 if (!in_subset_[i]) complement_of_subset_.push_back(i);
680 }
681 bound = min_outgoing_flow_helper_.ComputeMinOutgoingFlow(
682 complement_of_subset_);
683 } else {
684 bound = min_outgoing_flow_helper_.ComputeMinOutgoingFlow(subset);
685 }
686 if (bound > min_outgoing_flow) {
687 absl::StrAppend(&name, "Automatic");
688 min_outgoing_flow = bound;
689 }
690 }
691 if (subset_or_complement_size <
692 params_.routing_cut_subset_size_for_tight_binary_relation_bound()) {
693 int bound;
694 if (contain_depot) {
695 complement_of_subset_.clear();
696 for (int i = 0; i < num_nodes_; ++i) {
697 if (!in_subset_[i]) complement_of_subset_.push_back(i);
698 }
699 bound = min_outgoing_flow_helper_.ComputeTightMinOutgoingFlow(
700 complement_of_subset_);
701 } else {
702 bound = min_outgoing_flow_helper_.ComputeTightMinOutgoingFlow(subset);
703 }
704 if (bound > min_outgoing_flow) {
705 absl::StrAppend(&name, "AutomaticTight");
706 min_outgoing_flow = bound;
707 }
708 }
709
710 // Bounds coming from the demands_/capacity_ fields (if set).
711 std::vector<int> to_ignore_candidates;
712 if (!demands_.empty()) {
713 // If subset contains depot, we actually look at the subset complement to
714 // derive a bound on the outgoing flow. If we cannot reach the capacity
715 // given the demands in the subset, we can derive tighter bounds.
716 int64_t has_excessive_demands = false;
717 int64_t has_negative_demands = false;
718 int64_t sum_of_elements = 0;
719 std::vector<int64_t> elements;
720 const auto process_demand = [&](int64_t d) {
721 if (d < 0) has_negative_demands = true;
722 if (d > capacity_) has_excessive_demands = true;
723 sum_of_elements += d;
724 elements.push_back(d);
725 };
726 if (contain_depot) {
727 for (int n = 0; n < num_nodes_; ++n) {
728 if (!in_subset_[n]) {
729 process_demand(demands_[n]);
730 }
731 }
732 } else {
733 for (const int n : subset) {
734 process_demand(demands_[n]);
735 }
736 }
737
738 // Lets wait for these to disappear before adding cuts.
739 if (has_excessive_demands) return false;
740
741 // Try to tighten the capacity using DP. Note that there is no point doing
742 // anything if one route can serve all demands since then the bound is
743 // already tight.
744 //
745 // TODO(user): Compute a bound in the presence of negative demands?
746 bool exact_was_used = false;
747 int64_t tightened_capacity = capacity_;
748 if (!has_negative_demands && sum_of_elements > capacity_) {
749 max_bounded_subset_sum_.Reset(capacity_);
750 for (const int64_t e : elements) {
751 max_bounded_subset_sum_.Add(e);
752 }
753 tightened_capacity = max_bounded_subset_sum_.CurrentMax();
754
755 // If the complexity looks ok, try a more expensive DP than the quick one
756 // above.
757 if (max_bounded_subset_sum_exact_.ComplexityEstimate(
758 elements.size(), capacity_) < params_.routing_cut_dp_effort()) {
759 const int64_t exact =
760 max_bounded_subset_sum_exact_.MaxSubsetSum(elements, capacity_);
761 CHECK_LE(exact, tightened_capacity);
762 if (exact < tightened_capacity) {
763 tightened_capacity = exact;
764 exact_was_used = true;
765 }
766 }
767 }
768
769 const int64_t flow_lower_bound =
770 MathUtil::CeilOfRatio(sum_of_elements, tightened_capacity);
771 if (flow_lower_bound > min_outgoing_flow) {
772 min_outgoing_flow = flow_lower_bound;
773 absl::StrAppend(&name, exact_was_used ? "Tightened" : "Capacity");
774 }
775
776 if (!contain_depot && flow_lower_bound >= min_outgoing_flow) {
777 // We compute the biggest extra item that could fit in 'flow_lower_bound'
778 // bins. If the first (flow_lower_bound - 1) bins are tight, i.e. all
779 // their tightened_capacity is filled, then the last bin will have
780 // 'last_bin_fillin' stuff, which will leave 'space_left' to fit an extra
781 // 'item.
782 const int64_t last_bin_fillin =
783 sum_of_elements - (flow_lower_bound - 1) * tightened_capacity;
784 const int64_t space_left = capacity_ - last_bin_fillin;
785 DCHECK_GE(space_left, 0);
786 DCHECK_LT(space_left, capacity_);
787
788 // It is possible to make this cut stronger, using similar reasoning to
789 // the Multistar CVRP cuts: if there is a node n (other than the depot)
790 // outside of `subset`, with a demand that is greater than space_left,
791 // then the outgoing flow of (subset + n) is >= flow_lower_bound + 1.
792 // Using this, we can show that we still need `flow_lower_bound` on the
793 // outgoing arcs of `subset` other than those towards n (noted A in the
794 // diagram below):
795 //
796 // ^ ^
797 // | |
798 // ----------------
799 // <--| subset | n |-->
800 // ----------------
801 // | |
802 // v v
803 //
804 // \------A------/\--B--/
805 //
806 // By hypothesis, outgoing_flow(A) + outgoing_flow(B) > flow_lower_bound
807 // and, since n is not the depot, outgoing_flow(B) <= 1. Hence
808 // outgoing_flow(A) >= flow_lower_bound.
809 for (int n = 1; n < num_nodes_; ++n) {
810 if (in_subset_[n]) continue;
811 if (demands_[n] > space_left) {
812 to_ignore_candidates.push_back(n);
813 }
814 }
815 }
816 }
817
818 // Out of to_ignore_candidates, use an heuristic to pick one.
819 int ignore_arcs_with_head = -1;
820 if (!to_ignore_candidates.empty()) {
821 absl::StrAppend(&name, "Lifted");
822
823 // Compute the lp weight going from subset to the candidates.
824 absl::flat_hash_map<int, double> candidate_weights;
825 for (const int n : to_ignore_candidates) candidate_weights[n] = 0;
826 for (const auto arc : relevant_arcs_) {
827 if (in_subset_[arc.tail] && !in_subset_[arc.head]) {
828 auto it = candidate_weights.find(arc.head);
829 if (it == candidate_weights.end()) continue;
830 it->second += arc.lp_value;
831 }
832 }
833
834 // Pick the set of candidates with the highest lp weight.
835 std::vector<int> bests;
836 double best_weight = 0.0;
837 for (const int n : to_ignore_candidates) {
838 const double weight = candidate_weights.at(n);
839 if (bests.empty() || weight > best_weight) {
840 bests.clear();
841 bests.push_back(n);
842 best_weight = weight;
843 } else if (weight == best_weight) {
844 bests.push_back(n);
845 }
846 }
847
848 // Randomly pick if we have many "bests".
849 ignore_arcs_with_head =
850 bests.size() == 1
851 ? bests[0]
852 : bests[absl::Uniform<int>(*random_, 0, bests.size())];
853 }
854
855 // Compute the current outgoing flow out of the subset.
856 //
857 // This can take a significant portion of the running time, it is why it is
858 // faster to do it only on arcs with non-zero lp values which should be in
859 // linear number rather than the total number of arc which can be quadratic.
860 //
861 // TODO(user): For the symmetric case there is an even faster algo. See if
862 // it can be generalized to the asymmetric one if become needed.
863 // Reference is algo 6.4 of the "The Traveling Salesman Problem" book
864 // mentionned above.
865 double outgoing_flow = 0.0;
866 for (const auto arc : relevant_arcs_) {
867 if (in_subset_[arc.tail] && !in_subset_[arc.head]) {
868 if (arc.head == ignore_arcs_with_head) continue;
869 outgoing_flow += arc.lp_value;
870 }
871 }
872
873 // Add a cut if the current outgoing flow is not enough.
874 bool result = false;
875 if (outgoing_flow + 1e-2 < min_outgoing_flow) {
876 result = AddOutgoingCut(manager, name, subset.size(), in_subset_,
877 /*rhs_lower_bound=*/min_outgoing_flow,
878 ignore_arcs_with_head);
879 }
880
881 // Sparse clean up.
882 for (const int n : subset) in_subset_[n] = false;
883
884 return result;
885}
886
887bool OutgoingCutHelper::TryBlossomSubsetCut(
888 std::string name, absl::Span<const ArcWithLpValue> symmetrized_edges,
889 absl::Span<const int> subset, LinearConstraintManager* manager) {
890 DCHECK_GE(subset.size(), 1);
891 DCHECK_LT(subset.size(), num_nodes_);
892
893 // Initialize "in_subset" and the subset demands.
894 for (const int n : subset) in_subset_[n] = true;
895 auto cleanup = ::absl::MakeCleanup([subset, this]() {
896 for (const int n : subset) in_subset_[n] = false;
897 });
898
899 // The heuristic assumes non-duplicates arcs, otherwise they are all bundled
900 // together in the same symmetric edge, and the result is probably wrong.
901 absl::flat_hash_set<std::pair<int, int>> special_edges;
902 int num_inverted = 0;
903 double sum_inverted = 0.0;
904 double sum = 0.0;
905 double best_change = 1.0;
906 ArcWithLpValue const* best_swap = nullptr;
907 for (const ArcWithLpValue& arc : symmetrized_edges) {
908 if (in_subset_[arc.tail] == in_subset_[arc.head]) continue;
909
910 if (arc.lp_value > 0.5) {
911 ++num_inverted;
912 special_edges.insert({arc.tail, arc.head});
913 sum_inverted += 1.0 - arc.lp_value;
914 } else {
915 sum += arc.lp_value;
916 }
917
918 const double change = std::abs(2 * arc.lp_value - 1.0);
919 if (change < best_change) {
920 best_change = change;
921 best_swap = &arc;
922 }
923 }
924
925 // If the we don't have an odd number, we move the best edge from one set to
926 // the other.
927 if (num_inverted % 2 == 0) {
928 if (best_swap == nullptr) return false;
929 if (special_edges.contains({best_swap->tail, best_swap->head})) {
930 --num_inverted;
931 special_edges.erase({best_swap->tail, best_swap->head});
932 sum_inverted -= (1.0 - best_swap->lp_value);
933 sum += best_swap->lp_value;
934 } else {
935 ++num_inverted;
936 special_edges.insert({best_swap->tail, best_swap->head});
937 sum_inverted += (1.0 - best_swap->lp_value);
938 sum -= best_swap->lp_value;
939 }
940 }
941 if (sum + sum_inverted > 0.99) return false;
942
943 // For the route constraint, it is actually allowed to have circuit of size
944 // 2, so the reasoning is wrong if one of the edges touches the depot.
945 if (!demands_.empty()) {
946 for (const auto [tail, head] : special_edges) {
947 if (tail == 0) return false;
948 }
949 }
950
951 // If there is just one special edge, and all other node can be ignored, then
952 // the reasonning is wrong too since we can have a 2-cycle. In that case
953 // we enforce the constraint when an extra self-loop literal is at zero.
954 int best_optional_index = -1;
955 if (special_edges.size() == 1) {
956 int num_other_optional = 0;
957 const auto [special_tail, special_head] = *special_edges.begin();
958 for (int i = 0; i < tails_.size(); ++i) {
959 if (tails_[i] != heads_[i]) continue;
960 if (tails_[i] != special_head && tails_[i] != special_tail) {
961 ++num_other_optional;
962 if (best_optional_index == -1 ||
963 literal_lp_values_[i] < literal_lp_values_[best_optional_index]) {
964 best_optional_index = i;
965 }
966 }
967 }
968 if (num_other_optional + 2 < num_nodes_) best_optional_index = -1;
969 }
970
971 // Try to generate the cut.
972 //
973 // We deal with the corner case with duplicate arcs, or just one side of a
974 // "symmetric" edge present.
975 int num_actual_inverted = 0;
976 absl::flat_hash_set<std::pair<int, int>> processed_arcs;
977 LinearConstraintBuilder builder(encoder_, IntegerValue(1), kMaxIntegerValue);
978
979 // Add extra self-loop at zero enforcement if needed.
980 // TODO(user): Can we deal with the 2-cycle case in a better way?
981 if (best_optional_index != -1) {
982 absl::StrAppend(&name, "_opt");
983
984 // This is tricky: The normal cut assume x_e <=1, but in case of a single
985 // 2 cycle, x_e can be equal to 2. So we need a coeff of 2 to disable that
986 // cut.
987 CHECK(builder.AddLiteralTerm(literals_[best_optional_index],
988 IntegerValue(2)));
989 }
990
991 for (int i = 0; i < tails_.size(); ++i) {
992 if (tails_[i] == heads_[i]) continue;
993 if (in_subset_[tails_[i]] == in_subset_[heads_[i]]) continue;
994
995 const std::pair<int, int> key = {tails_[i], heads_[i]};
996 const std::pair<int, int> r_key = {heads_[i], tails_[i]};
997 const std::pair<int, int> s_key = std::min(key, r_key);
998 if (special_edges.contains(s_key) && !processed_arcs.contains(key)) {
999 processed_arcs.insert(key);
1000 CHECK(builder.AddLiteralTerm(literals_[i], IntegerValue(-1)));
1001 if (!processed_arcs.contains(r_key)) {
1002 ++num_actual_inverted;
1003 }
1004 continue;
1005 }
1006
1007 // Normal edge.
1008 CHECK(builder.AddLiteralTerm(literals_[i], IntegerValue(1)));
1009 }
1010 builder.AddConstant(IntegerValue(num_actual_inverted));
1011 if (num_actual_inverted % 2 == 0) return false;
1012
1013 return manager->AddCut(builder.Build(), name);
1014}
1015
1016} // namespace
1017
1018void GenerateInterestingSubsets(int num_nodes,
1019 absl::Span<const std::pair<int, int>> arcs,
1020 int stop_at_num_components,
1021 std::vector<int>* subset_data,
1022 std::vector<absl::Span<const int>>* subsets) {
1023 // We will do a union-find by adding one by one the arc of the lp solution
1024 // in the order above. Every intermediate set during this construction will
1025 // be a candidate for a cut.
1026 //
1027 // In parallel to the union-find, to efficiently reconstruct these sets (at
1028 // most num_nodes), we construct a "decomposition forest" of the different
1029 // connected components. Note that we don't exploit any asymmetric nature of
1030 // the graph here. This is exactly the algo 6.3 in the book above.
1031 int num_components = num_nodes;
1032 std::vector<int> parent(num_nodes);
1033 std::vector<int> root(num_nodes);
1034 for (int i = 0; i < num_nodes; ++i) {
1035 parent[i] = i;
1036 root[i] = i;
1037 }
1038 auto get_root_and_compress_path = [&root](int node) {
1039 int r = node;
1040 while (root[r] != r) r = root[r];
1041 while (root[node] != r) {
1042 const int next = root[node];
1043 root[node] = r;
1044 node = next;
1045 }
1046 return r;
1047 };
1048 for (const auto& [initial_tail, initial_head] : arcs) {
1049 if (num_components <= stop_at_num_components) break;
1050 const int tail = get_root_and_compress_path(initial_tail);
1051 const int head = get_root_and_compress_path(initial_head);
1052 if (tail != head) {
1053 // Update the decomposition forest, note that the number of nodes is
1054 // growing.
1055 const int new_node = parent.size();
1056 parent.push_back(new_node);
1057 parent[head] = new_node;
1058 parent[tail] = new_node;
1059 --num_components;
1060
1061 // It is important that the union-find representative is the same node.
1062 root.push_back(new_node);
1063 root[head] = new_node;
1064 root[tail] = new_node;
1065 }
1066 }
1067
1068 // For each node in the decomposition forest, try to add a cut for the set
1069 // formed by the nodes and its children. To do that efficiently, we first
1070 // order the nodes so that for each node in a tree, the set of children forms
1071 // a consecutive span in the subset_data vector. This vector just lists the
1072 // nodes in the "pre-order" graph traversal order. The Spans will point inside
1073 // the subset_data vector, it is why we initialize it once and for all.
1074 ExtractAllSubsetsFromForest(parent, subset_data, subsets,
1075 /*node_limit=*/num_nodes);
1076}
1077
1078void ExtractAllSubsetsFromForest(absl::Span<const int> parent,
1079 std::vector<int>* subset_data,
1080 std::vector<absl::Span<const int>>* subsets,
1081 int node_limit) {
1082 // To not reallocate memory since we need the span to point inside this
1083 // vector, we resize subset_data right away.
1084 int out_index = 0;
1085 const int num_nodes = parent.size();
1086 subset_data->resize(std::min(num_nodes, node_limit));
1087 subsets->clear();
1088
1089 // Starts by creating the corresponding graph and find the root.
1090 util::StaticGraph<int> graph(num_nodes, num_nodes - 1);
1091 for (int i = 0; i < num_nodes; ++i) {
1092 if (parent[i] != i) {
1093 graph.AddArc(parent[i], i);
1094 }
1095 }
1096 graph.Build();
1097
1098 // Perform a dfs on the rooted tree.
1099 // The subset_data will just be the node in post-order.
1100 std::vector<int> subtree_starts(num_nodes, -1);
1101 std::vector<int> stack;
1102 stack.reserve(num_nodes);
1103 for (int i = 0; i < parent.size(); ++i) {
1104 if (parent[i] != i) continue;
1105
1106 stack.push_back(i); // root.
1107 while (!stack.empty()) {
1108 const int node = stack.back();
1109
1110 // The node was already explored, output its subtree and pop it.
1111 if (subtree_starts[node] >= 0) {
1112 stack.pop_back();
1113 if (node < node_limit) {
1114 (*subset_data)[out_index++] = node;
1115 }
1116 const int start = subtree_starts[node];
1117 const int size = out_index - start;
1118 subsets->push_back(absl::MakeSpan(&(*subset_data)[start], size));
1119 continue;
1120 }
1121
1122 // Explore.
1123 subtree_starts[node] = out_index;
1124 for (const int child : graph[node]) {
1125 stack.push_back(child);
1126 }
1127 }
1128 }
1129}
1130
1131std::vector<int> ComputeGomoryHuTree(
1132 int num_nodes, absl::Span<const ArcWithLpValue> relevant_arcs) {
1133 // Initialize the graph. Note that we use only arcs with a relevant lp
1134 // value, so this should be small in practice.
1135 SimpleMaxFlow max_flow;
1136 for (const auto& [tail, head, lp_value] : relevant_arcs) {
1137 max_flow.AddArcWithCapacity(tail, head, std::round(1.0e6 * lp_value));
1138 max_flow.AddArcWithCapacity(head, tail, std::round(1.0e6 * lp_value));
1139 }
1140
1141 // Compute an equivalent max-flow tree, according to the paper.
1142 // This version should actually produce a Gomory-Hu cut tree.
1143 std::vector<int> min_cut_subset;
1144 std::vector<int> parent(num_nodes, 0);
1145 for (int s = 1; s < num_nodes; ++s) {
1146 const int t = parent[s];
1147 if (max_flow.Solve(s, t) != SimpleMaxFlow::OPTIMAL) break;
1148 max_flow.GetSourceSideMinCut(&min_cut_subset);
1149 bool parent_of_t_in_subset = false;
1150 for (const int i : min_cut_subset) {
1151 if (i == parent[t]) parent_of_t_in_subset = true;
1152 if (i != s && parent[i] == t) parent[i] = s;
1153 }
1154 if (parent_of_t_in_subset) {
1155 parent[s] = parent[t];
1156 parent[t] = s;
1157 }
1158 }
1159
1160 return parent;
1161}
1162
1163void SymmetrizeArcs(std::vector<ArcWithLpValue>* arcs) {
1164 for (ArcWithLpValue& arc : *arcs) {
1165 if (arc.tail <= arc.head) continue;
1166 std::swap(arc.tail, arc.head);
1167 }
1168 std::sort(arcs->begin(), arcs->end(),
1169 [](const ArcWithLpValue& a, const ArcWithLpValue& b) {
1170 return std::tie(a.tail, a.head) < std::tie(b.tail, b.head);
1171 });
1172
1173 int new_size = 0;
1174 int last_tail = -1;
1175 int last_head = -1;
1176 for (const ArcWithLpValue& arc : *arcs) {
1177 if (arc.tail == last_tail && arc.head == last_head) {
1178 (*arcs)[new_size - 1].lp_value += arc.lp_value;
1179 continue;
1180 }
1181 (*arcs)[new_size++] = arc;
1182 last_tail = arc.tail;
1183 last_head = arc.head;
1184 }
1185 arcs->resize(new_size);
1186}
1187
1188// We roughly follow the algorithm described in section 6 of "The Traveling
1189// Salesman Problem, A computational Study", David L. Applegate, Robert E.
1190// Bixby, Vasek Chvatal, William J. Cook.
1191//
1192// Note that this is mainly a "symmetric" case algo, but it does still work for
1193// the asymmetric case.
1194void SeparateSubtourInequalities(OutgoingCutHelper& helper,
1196 const int num_nodes = helper.num_nodes();
1197 if (num_nodes <= 2) return;
1198
1199 helper.InitializeForNewLpSolution(manager);
1200
1201 std::vector<int> subset_data;
1202 std::vector<absl::Span<const int>> subsets;
1203 GenerateInterestingSubsets(num_nodes, helper.ordered_arcs(),
1204 /*stop_at_num_components=*/2, &subset_data,
1205 &subsets);
1206
1207 const int depot = 0;
1208 if (helper.is_route_constraint()) {
1209 // Add the depot so that we have a trivial bound on the number of
1210 // vehicle.
1211 subsets.push_back(absl::MakeSpan(&depot, 1));
1212 }
1213
1214 // Hack/optim: we exploit the tree structure of the subsets to not add a cut
1215 // for a larger subset if we added a cut from one included in it.
1216 //
1217 // TODO(user): Currently if we add too many not so relevant cuts, our generic
1218 // MIP cut heuritic are way too slow on TSP/VRP problems.
1219 int last_added_start = -1;
1220
1221 // Process each subsets and add any violated cut.
1222 int num_added = 0;
1223 for (const absl::Span<const int> subset : subsets) {
1224 if (subset.size() <= 1) continue;
1225 const int start = static_cast<int>(subset.data() - subset_data.data());
1226 if (start <= last_added_start) continue;
1227 if (helper.TrySubsetCut("Circuit", subset, manager)) {
1228 ++num_added;
1229 last_added_start = start;
1230 }
1231 }
1232
1233 // If there were no cut added by the heuristic above, we try exact separation.
1234 //
1235 // With n-1 max_flow from a source to all destination, we can get the global
1236 // min-cut. Here, we use a slightly more advanced algorithm that will find a
1237 // min-cut for all possible pair of nodes. This is achieved by computing a
1238 // Gomory-Hu tree, still with n-1 max flow call.
1239 //
1240 // Note(user): Compared to any min-cut, these cut have some nice properties
1241 // since they are "included" in each other. This might help with combining
1242 // them within our generic IP cuts framework.
1243 //
1244 // TODO(user): I had an older version that tried the n-cuts generated during
1245 // the course of the algorithm. This could also be interesting. But it is
1246 // hard to tell with our current benchmark setup.
1247 if (num_added != 0) return;
1248 absl::Span<const ArcWithLpValue> symmetrized_relevant_arcs =
1249 helper.SymmetrizedRelevantArcs();
1250 std::vector<int> parent =
1251 ComputeGomoryHuTree(num_nodes, symmetrized_relevant_arcs);
1252
1253 // Try all interesting subset from the Gomory-Hu tree.
1254 ExtractAllSubsetsFromForest(parent, &subset_data, &subsets);
1255 last_added_start = -1;
1256 for (const absl::Span<const int> subset : subsets) {
1257 if (subset.size() <= 1) continue;
1258 if (subset.size() == num_nodes) continue;
1259 const int start = static_cast<int>(subset.data() - subset_data.data());
1260 if (start <= last_added_start) continue;
1261 if (helper.TrySubsetCut("CircuitExact", subset, manager)) {
1262 ++num_added;
1263 last_added_start = start;
1264 }
1265 }
1266
1267 // Exact separation of symmetric Blossom cut. We use the algorithm in the
1268 // paper: "A Faster Exact Separation Algorithm for Blossom Inequalities", Adam
1269 // N. Letchford, Gerhard Reinelt, Dirk Oliver Theis, 2004.
1270 if (num_added != 0) return;
1271 if (num_nodes <= 2) return;
1272 std::vector<ArcWithLpValue> for_blossom;
1273 for_blossom.reserve(symmetrized_relevant_arcs.size());
1274 for (ArcWithLpValue arc : symmetrized_relevant_arcs) {
1275 if (arc.lp_value > 0.5) arc.lp_value = 1.0 - arc.lp_value;
1276 if (arc.lp_value < 1e-6) continue;
1277 for_blossom.push_back(arc);
1278 }
1279 parent = ComputeGomoryHuTree(num_nodes, for_blossom);
1280 ExtractAllSubsetsFromForest(parent, &subset_data, &subsets);
1281 last_added_start = -1;
1282 for (const absl::Span<const int> subset : subsets) {
1283 if (subset.size() <= 1) continue;
1284 if (subset.size() == num_nodes) continue;
1285 const int start = static_cast<int>(subset.data() - subset_data.data());
1286 if (start <= last_added_start) continue;
1287 if (helper.TryBlossomSubsetCut("CircuitBlossom", symmetrized_relevant_arcs,
1288 subset, manager)) {
1289 ++num_added;
1290 last_added_start = start;
1291 }
1292 }
1293}
1294
1295namespace {
1296
1297// Returns for each literal its integer view, or the view of its negation.
1298std::vector<IntegerVariable> GetAssociatedVariables(
1299 absl::Span<const Literal> literals, Model* model) {
1300 auto* encoder = model->GetOrCreate<IntegerEncoder>();
1301 std::vector<IntegerVariable> result;
1302 for (const Literal l : literals) {
1303 const IntegerVariable direct_view = encoder->GetLiteralView(l);
1304 if (direct_view != kNoIntegerVariable) {
1305 result.push_back(direct_view);
1306 } else {
1307 result.push_back(encoder->GetLiteralView(l.Negated()));
1308 DCHECK_NE(result.back(), kNoIntegerVariable);
1309 }
1310 }
1311 return result;
1312}
1313
1314} // namespace
1315
1316// We use a basic algorithm to detect components that are not connected to the
1317// rest of the graph in the LP solution, and add cuts to force some arcs to
1318// enter and leave this component from outside.
1320 int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
1321 absl::Span<const Literal> literals, Model* model) {
1322 auto helper = std::make_unique<OutgoingCutHelper>(
1323 num_nodes, /*is_route_constraint=*/false, /*capacity=*/0,
1324 /*demands=*/absl::Span<const int64_t>(), tails, heads, literals, model);
1325 CutGenerator result;
1326 result.vars = GetAssociatedVariables(literals, model);
1327 result.generate_cuts =
1328 [helper = std::move(helper)](LinearConstraintManager* manager) {
1329 SeparateSubtourInequalities(*helper, manager);
1330 return true;
1331 };
1332 return result;
1333}
1334
1335CutGenerator CreateCVRPCutGenerator(int num_nodes, absl::Span<const int> tails,
1336 absl::Span<const int> heads,
1337 absl::Span<const Literal> literals,
1338 absl::Span<const int64_t> demands,
1339 int64_t capacity, Model* model) {
1340 auto helper = std::make_unique<OutgoingCutHelper>(
1341 num_nodes, /*is_route_constraint=*/true, capacity, demands, tails, heads,
1342 literals, model);
1343 CutGenerator result;
1344 result.vars = GetAssociatedVariables(literals, model);
1345 result.generate_cuts =
1346 [helper = std::move(helper)](LinearConstraintManager* manager) {
1347 SeparateSubtourInequalities(*helper, manager);
1348 return true;
1349 };
1350 return result;
1351}
1352
1353// This is really similar to SeparateSubtourInequalities, see the reference
1354// there.
1356 int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
1357 absl::Span<const AffineExpression> arc_capacities,
1358 std::function<void(const std::vector<bool>& in_subset,
1359 IntegerValue* min_incoming_flow,
1360 IntegerValue* min_outgoing_flow)>
1361 get_flows,
1363 LinearConstraintManager* manager, Model* model) {
1364 // We will collect only the arcs with a positive lp capacity value to speed up
1365 // some computation below.
1366 struct Arc {
1367 int tail;
1368 int head;
1369 double lp_value;
1370 IntegerValue offset;
1371 };
1372 std::vector<Arc> relevant_arcs;
1373
1374 // Often capacities have a coeff > 1.
1375 // We currently exploit this if all coeff have a gcd > 1.
1376 int64_t gcd = 0;
1377
1378 // Sort the arcs by non-increasing lp_values.
1379 std::vector<std::pair<double, int>> arc_by_decreasing_lp_values;
1380 for (int i = 0; i < arc_capacities.size(); ++i) {
1381 const double lp_value = arc_capacities[i].LpValue(lp_values);
1382 if (!arc_capacities[i].IsConstant()) {
1383 gcd = std::gcd(gcd, std::abs(arc_capacities[i].coeff.value()));
1384 }
1385 if (lp_value < 1e-6 && arc_capacities[i].constant == 0) continue;
1386 relevant_arcs.push_back(
1387 {tails[i], heads[i], lp_value, arc_capacities[i].constant});
1388 arc_by_decreasing_lp_values.push_back({lp_value, i});
1389 }
1390 if (gcd == 0) return;
1391 std::sort(arc_by_decreasing_lp_values.begin(),
1392 arc_by_decreasing_lp_values.end(),
1393 std::greater<std::pair<double, int>>());
1394
1395 std::vector<std::pair<int, int>> ordered_arcs;
1396 for (const auto& [score, arc] : arc_by_decreasing_lp_values) {
1397 if (tails[arc] == -1) continue;
1398 if (heads[arc] == -1) continue;
1399 ordered_arcs.push_back({tails[arc], heads[arc]});
1400 }
1401 std::vector<int> subset_data;
1402 std::vector<absl::Span<const int>> subsets;
1403 GenerateInterestingSubsets(num_nodes, ordered_arcs,
1404 /*stop_at_num_components=*/1, &subset_data,
1405 &subsets);
1406
1407 // Process each subsets and add any violated cut.
1408 std::vector<bool> in_subset(num_nodes, false);
1409 for (const absl::Span<const int> subset : subsets) {
1410 DCHECK(!subset.empty());
1411 DCHECK_LE(subset.size(), num_nodes);
1412
1413 // Initialize "in_subset" and the subset demands.
1414 for (const int n : subset) in_subset[n] = true;
1415
1416 IntegerValue min_incoming_flow;
1417 IntegerValue min_outgoing_flow;
1418 get_flows(in_subset, &min_incoming_flow, &min_outgoing_flow);
1419
1420 // We will sum the offset of all incoming/outgoing arc capacities.
1421 // Note that all arcs with a non-zero offset are part of relevant_arcs.
1422 IntegerValue incoming_offset(0);
1423 IntegerValue outgoing_offset(0);
1424
1425 // Compute the current flow in and out of the subset.
1426 //
1427 // This can take a significant portion of the running time, it is why it is
1428 // faster to do it only on arcs with non-zero lp values which should be in
1429 // linear number rather than the total number of arc which can be quadratic.
1430 double lp_outgoing_flow = 0.0;
1431 double lp_incoming_flow = 0.0;
1432 for (const auto arc : relevant_arcs) {
1433 if (arc.tail != -1 && in_subset[arc.tail]) {
1434 if (arc.head == -1 || !in_subset[arc.head]) {
1435 incoming_offset += arc.offset;
1436 lp_outgoing_flow += arc.lp_value;
1437 }
1438 } else {
1439 if (arc.head != -1 && in_subset[arc.head]) {
1440 outgoing_offset += arc.offset;
1441 lp_incoming_flow += arc.lp_value;
1442 }
1443 }
1444 }
1445
1446 // If the gcd is greater than one, because all variables are integer we
1447 // can round the flow lower bound to the next multiple of the gcd.
1448 //
1449 // TODO(user): Alternatively, try MIR heuristics if the coefficients in
1450 // the capacities are not all the same.
1451 if (gcd > 1) {
1452 const IntegerValue test_incoming = min_incoming_flow - incoming_offset;
1453 const IntegerValue new_incoming =
1454 CeilRatio(test_incoming, IntegerValue(gcd)) * IntegerValue(gcd);
1455 const IntegerValue incoming_delta = new_incoming - test_incoming;
1456 if (incoming_delta > 0) min_incoming_flow += incoming_delta;
1457 }
1458 if (gcd > 1) {
1459 const IntegerValue test_outgoing = min_outgoing_flow - outgoing_offset;
1460 const IntegerValue new_outgoing =
1461 CeilRatio(test_outgoing, IntegerValue(gcd)) * IntegerValue(gcd);
1462 const IntegerValue outgoing_delta = new_outgoing - test_outgoing;
1463 if (outgoing_delta > 0) min_outgoing_flow += outgoing_delta;
1464 }
1465
1466 if (lp_incoming_flow < ToDouble(min_incoming_flow) - 1e-6) {
1467 VLOG(2) << "INCOMING CUT " << lp_incoming_flow
1468 << " >= " << min_incoming_flow << " size " << subset.size()
1469 << " offset " << incoming_offset << " gcd " << gcd;
1470 LinearConstraintBuilder cut(model, min_incoming_flow, kMaxIntegerValue);
1471 for (int i = 0; i < tails.size(); ++i) {
1472 if ((tails[i] == -1 || !in_subset[tails[i]]) &&
1473 (heads[i] != -1 && in_subset[heads[i]])) {
1474 cut.AddTerm(arc_capacities[i], 1.0);
1475 }
1476 }
1477 manager->AddCut(cut.Build(), "IncomingFlow");
1478 }
1479
1480 if (lp_outgoing_flow < ToDouble(min_outgoing_flow) - 1e-6) {
1481 VLOG(2) << "OUGOING CUT " << lp_outgoing_flow
1482 << " >= " << min_outgoing_flow << " size " << subset.size()
1483 << " offset " << outgoing_offset << " gcd " << gcd;
1484 LinearConstraintBuilder cut(model, min_outgoing_flow, kMaxIntegerValue);
1485 for (int i = 0; i < tails.size(); ++i) {
1486 if ((tails[i] != -1 && in_subset[tails[i]]) &&
1487 (heads[i] == -1 || !in_subset[heads[i]])) {
1488 cut.AddTerm(arc_capacities[i], 1.0);
1489 }
1490 }
1491 manager->AddCut(cut.Build(), "OutgoingFlow");
1492 }
1493
1494 // Sparse clean up.
1495 for (const int n : subset) in_subset[n] = false;
1496 }
1497}
1498
1499CutGenerator CreateFlowCutGenerator(
1500 int num_nodes, const std::vector<int>& tails, const std::vector<int>& heads,
1501 const std::vector<AffineExpression>& arc_capacities,
1502 std::function<void(const std::vector<bool>& in_subset,
1503 IntegerValue* min_incoming_flow,
1504 IntegerValue* min_outgoing_flow)>
1505 get_flows,
1506 Model* model) {
1507 CutGenerator result;
1508 for (const AffineExpression expr : arc_capacities) {
1509 if (!expr.IsConstant()) result.vars.push_back(expr.var);
1510 }
1511 result.generate_cuts = [=](LinearConstraintManager* manager) {
1512 SeparateFlowInequalities(num_nodes, tails, heads, arc_capacities, get_flows,
1513 manager->LpValues(), manager, model);
1514 return true;
1515 };
1516 return result;
1517}
1518
1519} // namespace sat
1520} // namespace operations_research
Definition model.h:341
static IntegralType CeilOfRatio(IntegralType numerator, IntegralType denominator)
Definition mathutil.h:40
static IntegralType FloorOfRatio(IntegralType numerator, IntegralType denominator)
Definition mathutil.h:54
ArcIndex AddArcWithCapacity(NodeIndex tail, NodeIndex head, FlowQuantity capacity)
Definition max_flow.cc:27
Status Solve(NodeIndex source, NodeIndex sink)
Definition max_flow.cc:60
void GetSourceSideMinCut(std::vector< NodeIndex > *result)
Definition max_flow.cc:119
IntegerVariable GetLiteralView(Literal lit) const
Definition integer.h:247
void AddTerm(IntegerVariable var, IntegerValue coeff)
bool AddCut(LinearConstraint ct, std::string type_name, std::string extra_info="")
double ComplexityEstimate(int num_elements, int64_t bin_size)
Definition util.cc:968
int64_t MaxSubsetSum(absl::Span< const int64_t > elements, int64_t bin_size)
Definition util.cc:978
void Add(int64_t value)
Add a value to the base set for which subset sums will be taken.
Definition util.cc:507
int ComputeTightMinOutgoingFlow(absl::Span< const int > subset)
int ComputeMinOutgoingFlow(absl::Span< const int > subset)
MinOutgoingFlowHelper(int num_nodes, const std::vector< int > &tails, const std::vector< int > &heads, const std::vector< Literal > &literals, Model *model)
const VariablesAssignment & Assignment() const
Definition sat_base.h:462
ArcIndexType AddArc(NodeIndexType tail, NodeIndexType head)
Definition graph.h:1342
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
CutGenerator CreateStronglyConnectedGraphCutGenerator(int num_nodes, absl::Span< const int > tails, absl::Span< const int > heads, absl::Span< const Literal > literals, Model *model)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
bool operator==(const BoolArgumentProto &lhs, const BoolArgumentProto &rhs)
std::vector< int > ComputeGomoryHuTree(int num_nodes, absl::Span< const ArcWithLpValue > relevant_arcs)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Returns the vector of the negated variables.
Definition integer.cc:52
const IntegerVariable kNoIntegerVariable(-1)
CutGenerator CreateCVRPCutGenerator(int num_nodes, absl::Span< const int > tails, absl::Span< const int > heads, absl::Span< const Literal > literals, absl::Span< const int64_t > demands, int64_t capacity, Model *model)
CutGenerator CreateFlowCutGenerator(int num_nodes, const std::vector< int > &tails, const std::vector< int > &heads, const std::vector< AffineExpression > &arc_capacities, std::function< void(const std::vector< bool > &in_subset, IntegerValue *min_incoming_flow, IntegerValue *min_outgoing_flow)> get_flows, Model *model)
H AbslHashValue(H h, const IntVar &i)
– ABSL HASHING SUPPORT --------------------------------------------------—
Definition cp_model.h:515
void SeparateFlowInequalities(int num_nodes, absl::Span< const int > tails, absl::Span< const int > heads, absl::Span< const AffineExpression > arc_capacities, std::function< void(const std::vector< bool > &in_subset, IntegerValue *min_incoming_flow, IntegerValue *min_outgoing_flow)> get_flows, const util_intops::StrongVector< IntegerVariable, double > &lp_values, LinearConstraintManager *manager, Model *model)
void SymmetrizeArcs(std::vector< ArcWithLpValue > *arcs)
void ExtractAllSubsetsFromForest(absl::Span< const int > parent, std::vector< int > *subset_data, std::vector< absl::Span< const int > > *subsets, int node_limit)
void GenerateInterestingSubsets(int num_nodes, absl::Span< const std::pair< int, int > > arcs, int stop_at_num_components, std::vector< int > *subset_data, std::vector< absl::Span< const int > > *subsets)
double ToDouble(IntegerValue value)
void SeparateSubtourInequalities(OutgoingCutHelper &helper, LinearConstraintManager *manager)
In SWIG mode, we don't want anything besides these top-level includes.
absl::AnyInvocable< bool(LinearConstraintManager *manager)> generate_cuts
Definition cuts.h:59
std::vector< IntegerVariable > vars
Definition cuts.h:58