Google OR-Tools v9.11
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-2024 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 <string>
22#include <tuple>
23#include <utility>
24#include <vector>
25
26#include "absl/cleanup/cleanup.h"
27#include "absl/container/inlined_vector.h"
28#include "absl/log/check.h"
29#include "absl/strings/str_cat.h"
30#include "absl/types/span.h"
34#include "ortools/graph/graph.h"
36#include "ortools/sat/cuts.h"
37#include "ortools/sat/integer.h"
40#include "ortools/sat/model.h"
42#include "ortools/sat/util.h"
44
45namespace operations_research {
46namespace sat {
47
48namespace {
49
50class OutgoingCutHelper {
51 public:
52 OutgoingCutHelper(int num_nodes, int64_t capacity,
53 absl::Span<const int64_t> demands,
54 const std::vector<int>& tails,
55 const std::vector<int>& heads,
56 const std::vector<Literal>& literals,
57 const std::vector<double>& literal_lp_values,
58 const std::vector<ArcWithLpValue>& relevant_arcs,
59 LinearConstraintManager* manager, Model* model)
60 : num_nodes_(num_nodes),
61 capacity_(capacity),
62 demands_(demands),
63 tails_(tails),
64 heads_(heads),
65 literals_(literals),
66 literal_lp_values_(literal_lp_values),
67 relevant_arcs_(relevant_arcs),
68 manager_(manager),
69 encoder_(model->GetOrCreate<IntegerEncoder>()) {
70 in_subset_.assign(num_nodes, false);
71
72 // Compute the total demands in order to know the minimum incoming/outgoing
73 // flow.
74 for (const int64_t demand : demands) total_demand_ += demand;
75 }
76
77 // Try to add an outgoing cut from the given subset.
78 bool TrySubsetCut(std::string name, absl::Span<const int> subset);
79
80 // If we look at the symmetrized version (tail <-> head = tail->head +
81 // head->tail) and we split all the edges between a subset of nodes S and the
82 // outside into a set A and the other d(S)\A, and |A| is odd, we have a
83 // constraint of the form:
84 // "all edge of A at 1" => sum other edges >= 1.
85 // This is because a cycle or multiple-cycle must go in/out an even number
86 // of time. This enforced constraint simply linearize to:
87 // sum_d(S)\A x_e + sum_A (1 - x_e) >= 1.
88 //
89 // Given a subset of nodes, it is easy to identify the best subset A of edge
90 // to consider.
91 bool TryBlossomSubsetCut(std::string name,
92 absl::Span<const ArcWithLpValue> symmetrized_edges,
93 absl::Span<const int> subset);
94
95 private:
96 // Add a cut of the form Sum_{outgoing arcs from S} lp >= rhs_lower_bound.
97 //
98 // Note that we used to also add the same cut for the incoming arcs, but
99 // because of flow conservation on these problems, the outgoing flow is always
100 // the same as the incoming flow, so adding this extra cut doesn't seem
101 // relevant.
102 bool AddOutgoingCut(std::string name, int subset_size,
103 const std::vector<bool>& in_subset,
104 int64_t rhs_lower_bound);
105
106 const int num_nodes_;
107 const int64_t capacity_;
108 const absl::Span<const int64_t> demands_;
109 const std::vector<int>& tails_;
110 const std::vector<int>& heads_;
111 const std::vector<Literal>& literals_;
112 const std::vector<double>& literal_lp_values_;
113 const std::vector<ArcWithLpValue>& relevant_arcs_;
114 LinearConstraintManager* manager_;
115 IntegerEncoder* encoder_;
116
117 int64_t total_demand_ = 0;
118 std::vector<bool> in_subset_;
119};
120
121bool OutgoingCutHelper::AddOutgoingCut(std::string name, int subset_size,
122 const std::vector<bool>& in_subset,
123 int64_t rhs_lower_bound) {
124 // A node is said to be optional if it can be excluded from the subcircuit,
125 // in which case there is a self-loop on that node.
126 // If there are optional nodes, use extended formula:
127 // sum(cut) >= 1 - optional_loop_in - optional_loop_out
128 // where optional_loop_in's node is in subset, optional_loop_out's is out.
129 // TODO(user): Favor optional loops fixed to zero at root.
130 int num_optional_nodes_in = 0;
131 int num_optional_nodes_out = 0;
132 int optional_loop_in = -1;
133 int optional_loop_out = -1;
134 for (int i = 0; i < tails_.size(); ++i) {
135 if (tails_[i] != heads_[i]) continue;
136 if (in_subset[tails_[i]]) {
137 num_optional_nodes_in++;
138 if (optional_loop_in == -1 ||
139 literal_lp_values_[i] < literal_lp_values_[optional_loop_in]) {
140 optional_loop_in = i;
141 }
142 } else {
143 num_optional_nodes_out++;
144 if (optional_loop_out == -1 ||
145 literal_lp_values_[i] < literal_lp_values_[optional_loop_out]) {
146 optional_loop_out = i;
147 }
148 }
149 }
150
151 // TODO(user): The lower bound for CVRP is computed assuming all nodes must be
152 // served, if it is > 1 we lower it to one in the presence of optional nodes.
153 if (num_optional_nodes_in + num_optional_nodes_out > 0) {
154 CHECK_GE(rhs_lower_bound, 1);
155 rhs_lower_bound = 1;
156 }
157
158 // We create the cut and rely on AddCut() for computing its efficacy and
159 // rejecting it if it is bad.
160 LinearConstraintBuilder outgoing(encoder_, IntegerValue(rhs_lower_bound),
161 kMaxIntegerValue);
162
163 // Add outgoing arcs, compute outgoing flow.
164 for (int i = 0; i < tails_.size(); ++i) {
165 if (in_subset[tails_[i]] && !in_subset[heads_[i]]) {
166 CHECK(outgoing.AddLiteralTerm(literals_[i], IntegerValue(1)));
167 }
168 }
169
170 // Support optional nodes if any.
171 if (num_optional_nodes_in + num_optional_nodes_out > 0) {
172 // When all optionals of one side are excluded in lp solution, no cut.
173 if (num_optional_nodes_in == subset_size &&
174 (optional_loop_in == -1 ||
175 literal_lp_values_[optional_loop_in] > 1.0 - 1e-6)) {
176 return false;
177 }
178 if (num_optional_nodes_out == num_nodes_ - subset_size &&
179 (optional_loop_out == -1 ||
180 literal_lp_values_[optional_loop_out] > 1.0 - 1e-6)) {
181 return false;
182 }
183
184 // There is no mandatory node in subset, add optional_loop_in.
185 if (num_optional_nodes_in == subset_size) {
186 CHECK(outgoing.AddLiteralTerm(literals_[optional_loop_in],
187 IntegerValue(1)));
188 }
189
190 // There is no mandatory node out of subset, add optional_loop_out.
191 if (num_optional_nodes_out == num_nodes_ - subset_size) {
192 CHECK(outgoing.AddLiteralTerm(literals_[optional_loop_out],
193 IntegerValue(1)));
194 }
195 }
196
197 return manager_->AddCut(outgoing.Build(), name);
198}
199
200bool OutgoingCutHelper::TrySubsetCut(std::string name,
201 absl::Span<const int> subset) {
202 DCHECK_GE(subset.size(), 1);
203 DCHECK_LT(subset.size(), num_nodes_);
204
205 // These fields will be left untouched if demands.empty().
206 bool contain_depot = false;
207 int64_t subset_demand = 0;
208
209 // Initialize "in_subset" and the subset demands.
210 for (const int n : subset) {
211 in_subset_[n] = true;
212 if (!demands_.empty()) {
213 if (n == 0) contain_depot = true;
214 subset_demand += demands_[n];
215 }
216 }
217
218 // Compute a lower bound on the outgoing flow.
219 //
220 // TODO(user): This lower bound assume all nodes in subset must be served.
221 // If this is not the case, we are really defensive in AddOutgoingCut().
222 // Improve depending on where the self-loop are.
223 //
224 // TODO(user): It could be very interesting to see if this "min outgoing
225 // flow" cannot be automatically infered from the constraint in the
226 // precedence graph. This might work if we assume that any kind of path
227 // cumul constraint is encoded with constraints:
228 // [edge => value_head >= value_tail + edge_weight].
229 // We could take the minimum incoming edge weight per node in the set, and
230 // use the cumul variable domain to infer some capacity.
231 int64_t min_outgoing_flow = 1;
232 if (!demands_.empty()) {
233 min_outgoing_flow =
234 contain_depot ? CeilOfRatio(total_demand_ - subset_demand, capacity_)
235 : CeilOfRatio(subset_demand, capacity_);
236 }
237
238 // We still need to serve nodes with a demand of zero, and in the corner
239 // case where all node in subset have a zero demand, the formula above
240 // result in a min_outgoing_flow of zero.
241 min_outgoing_flow = std::max(min_outgoing_flow, int64_t{1});
242
243 // Compute the current outgoing flow out of the subset.
244 //
245 // This can take a significant portion of the running time, it is why it is
246 // faster to do it only on arcs with non-zero lp values which should be in
247 // linear number rather than the total number of arc which can be quadratic.
248 //
249 // TODO(user): For the symmetric case there is an even faster algo. See if
250 // it can be generalized to the asymmetric one if become needed.
251 // Reference is algo 6.4 of the "The Traveling Salesman Problem" book
252 // mentionned above.
253 double outgoing_flow = 0.0;
254 for (const auto arc : relevant_arcs_) {
255 if (in_subset_[arc.tail] && !in_subset_[arc.head]) {
256 outgoing_flow += arc.lp_value;
257 }
258 }
259
260 // Add a cut if the current outgoing flow is not enough.
261 bool result = false;
262 if (outgoing_flow + 1e-2 < min_outgoing_flow) {
263 result = AddOutgoingCut(name, subset.size(), in_subset_,
264 /*rhs_lower_bound=*/min_outgoing_flow);
265 }
266
267 // Sparse clean up.
268 for (const int n : subset) in_subset_[n] = false;
269
270 return result;
271}
272
273bool OutgoingCutHelper::TryBlossomSubsetCut(
274 std::string name, absl::Span<const ArcWithLpValue> symmetrized_edges,
275 absl::Span<const int> subset) {
276 DCHECK_GE(subset.size(), 1);
277 DCHECK_LT(subset.size(), num_nodes_);
278
279 // Initialize "in_subset" and the subset demands.
280 for (const int n : subset) in_subset_[n] = true;
281 auto cleanup = ::absl::MakeCleanup([subset, this]() {
282 for (const int n : subset) in_subset_[n] = false;
283 });
284
285 // The heuristic assumes non-duplicates arcs, otherwise they are all bundled
286 // together in the same symmetric edge, and the result is probably wrong.
287 absl::flat_hash_set<std::pair<int, int>> special_edges;
288 int num_inverted = 0;
289 double sum_inverted = 0.0;
290 double sum = 0.0;
291 double best_change = 1.0;
292 ArcWithLpValue const* best_swap = nullptr;
293 for (const ArcWithLpValue& arc : symmetrized_edges) {
294 if (in_subset_[arc.tail] == in_subset_[arc.head]) continue;
295
296 if (arc.lp_value > 0.5) {
297 ++num_inverted;
298 special_edges.insert({arc.tail, arc.head});
299 sum_inverted += 1.0 - arc.lp_value;
300 } else {
301 sum += arc.lp_value;
302 }
303
304 const double change = std::abs(2 * arc.lp_value - 1.0);
305 if (change < best_change) {
306 best_change = change;
307 best_swap = &arc;
308 }
309 }
310
311 // If the we don't have an odd number, we move the best edge from one set to
312 // the other.
313 if (num_inverted % 2 == 0) {
314 if (best_swap == nullptr) return false;
315 if (special_edges.contains({best_swap->tail, best_swap->head})) {
316 --num_inverted;
317 special_edges.erase({best_swap->tail, best_swap->head});
318 sum_inverted -= (1.0 - best_swap->lp_value);
319 sum += best_swap->lp_value;
320 } else {
321 ++num_inverted;
322 special_edges.insert({best_swap->tail, best_swap->head});
323 sum_inverted += (1.0 - best_swap->lp_value);
324 sum -= best_swap->lp_value;
325 }
326 }
327 if (sum + sum_inverted > 0.99) return false;
328
329 // For the route constraint, it is actually allowed to have circuit of size
330 // 2, so the reasoning is wrong if one of the edges touches the depot.
331 if (!demands_.empty()) {
332 for (const auto [tail, head] : special_edges) {
333 if (tail == 0) return false;
334 }
335 }
336
337 // If there is just one special edge, and all other node can be ignored, then
338 // the reasonning is wrong too since we can have a 2-cycle. In that case
339 // we enforce the constraint when an extra self-loop literal is at zero.
340 int best_optional_index = -1;
341 if (special_edges.size() == 1) {
342 int num_other_optional = 0;
343 const auto [special_tail, special_head] = *special_edges.begin();
344 for (int i = 0; i < tails_.size(); ++i) {
345 if (tails_[i] != heads_[i]) continue;
346 if (tails_[i] != special_head && tails_[i] != special_tail) {
347 ++num_other_optional;
348 if (best_optional_index == -1 ||
349 literal_lp_values_[i] < literal_lp_values_[best_optional_index]) {
350 best_optional_index = i;
351 }
352 }
353 }
354 if (num_other_optional + 2 < num_nodes_) best_optional_index = -1;
355 }
356
357 // Try to generate the cut.
358 //
359 // We deal with the corner case with duplicate arcs, or just one side of a
360 // "symmetric" edge present.
361 int num_actual_inverted = 0;
362 absl::flat_hash_set<std::pair<int, int>> processed_arcs;
363 LinearConstraintBuilder builder(encoder_, IntegerValue(1), kMaxIntegerValue);
364
365 // Add extra self-loop at zero enforcement if needed.
366 // TODO(user): Can we deal with the 2-cycle case in a better way?
367 if (best_optional_index != -1) {
368 absl::StrAppend(&name, "_opt");
369
370 // This is tricky: The normal cut assume x_e <=1, but in case of a single
371 // 2 cycle, x_e can be equal to 2. So we need a coeff of 2 to disable that
372 // cut.
373 CHECK(builder.AddLiteralTerm(literals_[best_optional_index],
374 IntegerValue(2)));
375 }
376
377 for (int i = 0; i < tails_.size(); ++i) {
378 if (tails_[i] == heads_[i]) continue;
379 if (in_subset_[tails_[i]] == in_subset_[heads_[i]]) continue;
380
381 const std::pair<int, int> key = {tails_[i], heads_[i]};
382 const std::pair<int, int> r_key = {heads_[i], tails_[i]};
383 const std::pair<int, int> s_key = std::min(key, r_key);
384 if (special_edges.contains(s_key) && !processed_arcs.contains(key)) {
385 processed_arcs.insert(key);
386 CHECK(builder.AddLiteralTerm(literals_[i], IntegerValue(-1)));
387 if (!processed_arcs.contains(r_key)) {
388 ++num_actual_inverted;
389 }
390 continue;
391 }
392
393 // Normal edge.
394 CHECK(builder.AddLiteralTerm(literals_[i], IntegerValue(1)));
395 }
396 builder.AddConstant(IntegerValue(num_actual_inverted));
397 if (num_actual_inverted % 2 == 0) return false;
398
399 return manager_->AddCut(builder.Build(), name);
400}
401
402} // namespace
403
404void GenerateInterestingSubsets(int num_nodes,
405 const std::vector<std::pair<int, int>>& arcs,
406 int stop_at_num_components,
407 std::vector<int>* subset_data,
408 std::vector<absl::Span<const int>>* subsets) {
409 // We will do a union-find by adding one by one the arc of the lp solution
410 // in the order above. Every intermediate set during this construction will
411 // be a candidate for a cut.
412 //
413 // In parallel to the union-find, to efficiently reconstruct these sets (at
414 // most num_nodes), we construct a "decomposition forest" of the different
415 // connected components. Note that we don't exploit any asymmetric nature of
416 // the graph here. This is exactly the algo 6.3 in the book above.
417 int num_components = num_nodes;
418 std::vector<int> parent(num_nodes);
419 std::vector<int> root(num_nodes);
420 for (int i = 0; i < num_nodes; ++i) {
421 parent[i] = i;
422 root[i] = i;
423 }
424 auto get_root_and_compress_path = [&root](int node) {
425 int r = node;
426 while (root[r] != r) r = root[r];
427 while (root[node] != r) {
428 const int next = root[node];
429 root[node] = r;
430 node = next;
431 }
432 return r;
433 };
434 for (const auto& [initial_tail, initial_head] : arcs) {
435 if (num_components <= stop_at_num_components) break;
436 const int tail = get_root_and_compress_path(initial_tail);
437 const int head = get_root_and_compress_path(initial_head);
438 if (tail != head) {
439 // Update the decomposition forest, note that the number of nodes is
440 // growing.
441 const int new_node = parent.size();
442 parent.push_back(new_node);
443 parent[head] = new_node;
444 parent[tail] = new_node;
445 --num_components;
446
447 // It is important that the union-find representative is the same node.
448 root.push_back(new_node);
449 root[head] = new_node;
450 root[tail] = new_node;
451 }
452 }
453
454 // For each node in the decomposition forest, try to add a cut for the set
455 // formed by the nodes and its children. To do that efficiently, we first
456 // order the nodes so that for each node in a tree, the set of children forms
457 // a consecutive span in the subset_data vector. This vector just lists the
458 // nodes in the "pre-order" graph traversal order. The Spans will point inside
459 // the subset_data vector, it is why we initialize it once and for all.
460 ExtractAllSubsetsFromForest(parent, subset_data, subsets,
461 /*node_limit=*/num_nodes);
462}
463
464void ExtractAllSubsetsFromForest(const std::vector<int>& parent,
465 std::vector<int>* subset_data,
466 std::vector<absl::Span<const int>>* subsets,
467 int node_limit) {
468 // To not reallocate memory since we need the span to point inside this
469 // vector, we resize subset_data right away.
470 int out_index = 0;
471 const int num_nodes = parent.size();
472 subset_data->resize(std::min(num_nodes, node_limit));
473 subsets->clear();
474
475 // Starts by creating the corresponding graph and find the root.
476 util::StaticGraph<int> graph(num_nodes, num_nodes - 1);
477 for (int i = 0; i < num_nodes; ++i) {
478 if (parent[i] != i) {
479 graph.AddArc(parent[i], i);
480 }
481 }
482 graph.Build();
483
484 // Perform a dfs on the rooted tree.
485 // The subset_data will just be the node in post-order.
486 std::vector<int> subtree_starts(num_nodes, -1);
487 std::vector<int> stack;
488 stack.reserve(num_nodes);
489 for (int i = 0; i < parent.size(); ++i) {
490 if (parent[i] != i) continue;
491
492 stack.push_back(i); // root.
493 while (!stack.empty()) {
494 const int node = stack.back();
495
496 // The node was already explored, output its subtree and pop it.
497 if (subtree_starts[node] >= 0) {
498 stack.pop_back();
499 if (node < node_limit) {
500 (*subset_data)[out_index++] = node;
501 }
502 const int start = subtree_starts[node];
503 const int size = out_index - start;
504 subsets->push_back(absl::MakeSpan(&(*subset_data)[start], size));
505 continue;
506 }
507
508 // Explore.
509 subtree_starts[node] = out_index;
510 for (const int child : graph[node]) {
511 stack.push_back(child);
512 }
513 }
514 }
515}
516
517std::vector<int> ComputeGomoryHuTree(
518 int num_nodes, const std::vector<ArcWithLpValue>& relevant_arcs) {
519 // Initialize the graph. Note that we use only arcs with a relevant lp
520 // value, so this should be small in practice.
521 SimpleMaxFlow max_flow;
522 for (const auto& [tail, head, lp_value] : relevant_arcs) {
523 max_flow.AddArcWithCapacity(tail, head, std::round(1.0e6 * lp_value));
524 max_flow.AddArcWithCapacity(head, tail, std::round(1.0e6 * lp_value));
525 }
526
527 // Compute an equivalent max-flow tree, according to the paper.
528 // This version should actually produce a Gomory-Hu cut tree.
529 std::vector<int> min_cut_subset;
530 std::vector<int> parent(num_nodes, 0);
531 for (int s = 1; s < num_nodes; ++s) {
532 const int t = parent[s];
533 if (max_flow.Solve(s, t) != SimpleMaxFlow::OPTIMAL) break;
534 max_flow.GetSourceSideMinCut(&min_cut_subset);
535 bool parent_of_t_in_subset = false;
536 for (const int i : min_cut_subset) {
537 if (i == parent[t]) parent_of_t_in_subset = true;
538 if (i != s && parent[i] == t) parent[i] = s;
539 }
540 if (parent_of_t_in_subset) {
541 parent[s] = parent[t];
542 parent[t] = s;
543 }
544 }
545
546 return parent;
547}
548
549void SymmetrizeArcs(std::vector<ArcWithLpValue>* arcs) {
550 for (ArcWithLpValue& arc : *arcs) {
551 if (arc.tail <= arc.head) continue;
552 std::swap(arc.tail, arc.head);
553 }
554 std::sort(arcs->begin(), arcs->end(),
555 [](const ArcWithLpValue& a, const ArcWithLpValue& b) {
556 return std::tie(a.tail, a.head) < std::tie(b.tail, b.head);
557 });
558
559 int new_size = 0;
560 int last_tail = -1;
561 int last_head = -1;
562 for (const ArcWithLpValue& arc : *arcs) {
563 if (arc.tail == last_tail && arc.head == last_head) {
564 (*arcs)[new_size - 1].lp_value += arc.lp_value;
565 continue;
566 }
567 (*arcs)[new_size++] = arc;
568 last_tail = arc.tail;
569 last_head = arc.head;
570 }
571 arcs->resize(new_size);
572}
573
574// We roughly follow the algorithm described in section 6 of "The Traveling
575// Salesman Problem, A computational Study", David L. Applegate, Robert E.
576// Bixby, Vasek Chvatal, William J. Cook.
577//
578// Note that this is mainly a "symmetric" case algo, but it does still work for
579// the asymmetric case.
581 int num_nodes, const std::vector<int>& tails, const std::vector<int>& heads,
582 const std::vector<Literal>& literals, absl::Span<const int64_t> demands,
583 int64_t capacity, LinearConstraintManager* manager, Model* model) {
584 if (num_nodes <= 2) return;
585
586 // We will collect only the arcs with a positive lp_values to speed up some
587 // computation below.
588 std::vector<ArcWithLpValue> relevant_arcs;
589
590 // Sort the arcs by non-increasing lp_values.
591 const auto& lp_values = manager->LpValues();
592 std::vector<double> literal_lp_values(literals.size());
593 std::vector<std::pair<double, int>> arc_by_decreasing_lp_values;
594 auto* encoder = model->GetOrCreate<IntegerEncoder>();
595 for (int i = 0; i < literals.size(); ++i) {
596 double lp_value;
597 const IntegerVariable direct_view = encoder->GetLiteralView(literals[i]);
598 if (direct_view != kNoIntegerVariable) {
599 lp_value = lp_values[direct_view];
600 } else {
601 lp_value =
602 1.0 - lp_values[encoder->GetLiteralView(literals[i].Negated())];
603 }
604 literal_lp_values[i] = lp_value;
605
606 if (lp_value < 1e-6) continue;
607 relevant_arcs.push_back({tails[i], heads[i], lp_value});
608 arc_by_decreasing_lp_values.push_back({lp_value, i});
609 }
610 std::sort(arc_by_decreasing_lp_values.begin(),
611 arc_by_decreasing_lp_values.end(),
612 std::greater<std::pair<double, int>>());
613
614 std::vector<std::pair<int, int>> ordered_arcs;
615 for (const auto& [score, arc] : arc_by_decreasing_lp_values) {
616 ordered_arcs.push_back({tails[arc], heads[arc]});
617 }
618 std::vector<int> subset_data;
619 std::vector<absl::Span<const int>> subsets;
620 GenerateInterestingSubsets(num_nodes, ordered_arcs,
621 /*stop_at_num_components=*/2, &subset_data,
622 &subsets);
623
624 const int depot = 0;
625 if (!demands.empty()) {
626 // Add the depot so that we have a trivial bound on the number of
627 // vehicle.
628 subsets.push_back(absl::MakeSpan(&depot, 1));
629 }
630
631 OutgoingCutHelper helper(num_nodes, capacity, demands, tails, heads, literals,
632 literal_lp_values, relevant_arcs, manager, model);
633
634 // Hack/optim: we exploit the tree structure of the subsets to not add a cut
635 // for a larger subset if we added a cut from one included in it.
636 //
637 // TODO(user): Currently if we add too many not so relevant cuts, our generic
638 // MIP cut heuritic are way too slow on TSP/VRP problems.
639 int last_added_start = -1;
640
641 // Process each subsets and add any violated cut.
642 int num_added = 0;
643 for (const absl::Span<const int> subset : subsets) {
644 if (subset.size() <= 1) continue;
645 const int start = static_cast<int>(subset.data() - subset_data.data());
646 if (start <= last_added_start) continue;
647 if (helper.TrySubsetCut("Circuit", subset)) {
648 ++num_added;
649 last_added_start = start;
650 }
651 }
652
653 // If there were no cut added by the heuristic above, we try exact separation.
654 //
655 // With n-1 max_flow from a source to all destination, we can get the global
656 // min-cut. Here, we use a slightly more advanced algorithm that will find a
657 // min-cut for all possible pair of nodes. This is achieved by computing a
658 // Gomory-Hu tree, still with n-1 max flow call.
659 //
660 // Note(user): Compared to any min-cut, these cut have some nice properties
661 // since they are "included" in each other. This might help with combining
662 // them within our generic IP cuts framework.
663 //
664 // TODO(user): I had an older version that tried the n-cuts generated during
665 // the course of the algorithm. This could also be interesting. But it is
666 // hard to tell with our current benchmark setup.
667 if (num_added != 0) return;
668 SymmetrizeArcs(&relevant_arcs);
669 std::vector<int> parent = ComputeGomoryHuTree(num_nodes, relevant_arcs);
670
671 // Try all interesting subset from the Gomory-Hu tree.
672 ExtractAllSubsetsFromForest(parent, &subset_data, &subsets);
673 last_added_start = -1;
674 for (const absl::Span<const int> subset : subsets) {
675 if (subset.size() <= 1) continue;
676 if (subset.size() == num_nodes) continue;
677 const int start = static_cast<int>(subset.data() - subset_data.data());
678 if (start <= last_added_start) continue;
679 if (helper.TrySubsetCut("CircuitExact", subset)) {
680 ++num_added;
681 last_added_start = start;
682 }
683 }
684
685 // Exact separation of symmetric Blossom cut. We use the algorithm in the
686 // paper: "A Faster Exact Separation Algorithm for Blossom Inequalities", Adam
687 // N. Letchford, Gerhard Reinelt, Dirk Oliver Theis, 2004.
688 //
689 // Note that the "relevant_arcs" were symmetrized above.
690 if (num_added != 0) return;
691 if (num_nodes <= 2) return;
692 std::vector<ArcWithLpValue> for_blossom;
693 for_blossom.reserve(relevant_arcs.size());
694 for (ArcWithLpValue arc : relevant_arcs) {
695 if (arc.lp_value > 0.5) arc.lp_value = 1.0 - arc.lp_value;
696 if (arc.lp_value < 1e-6) continue;
697 for_blossom.push_back(arc);
698 }
699 parent = ComputeGomoryHuTree(num_nodes, for_blossom);
700 ExtractAllSubsetsFromForest(parent, &subset_data, &subsets);
701 last_added_start = -1;
702 for (const absl::Span<const int> subset : subsets) {
703 if (subset.size() <= 1) continue;
704 if (subset.size() == num_nodes) continue;
705 const int start = static_cast<int>(subset.data() - subset_data.data());
706 if (start <= last_added_start) continue;
707 if (helper.TryBlossomSubsetCut("CircuitBlossom", relevant_arcs, subset)) {
708 ++num_added;
709 last_added_start = start;
710 }
711 }
712}
713
714namespace {
715
716// Returns for each literal its integer view, or the view of its negation.
717std::vector<IntegerVariable> GetAssociatedVariables(
718 absl::Span<const Literal> literals, Model* model) {
719 auto* encoder = model->GetOrCreate<IntegerEncoder>();
720 std::vector<IntegerVariable> result;
721 for (const Literal l : literals) {
722 const IntegerVariable direct_view = encoder->GetLiteralView(l);
723 if (direct_view != kNoIntegerVariable) {
724 result.push_back(direct_view);
725 } else {
726 result.push_back(encoder->GetLiteralView(l.Negated()));
727 DCHECK_NE(result.back(), kNoIntegerVariable);
728 }
729 }
730 return result;
731}
732
733// This is especially useful to remove fixed self loop.
734void FilterFalseArcsAtLevelZero(std::vector<int>& tails,
735 std::vector<int>& heads,
736 std::vector<Literal>& literals, Model* model) {
737 const Trail& trail = *model->GetOrCreate<Trail>();
738 if (trail.CurrentDecisionLevel() != 0) return;
739
740 int new_size = 0;
741 const int size = static_cast<int>(tails.size());
742 const VariablesAssignment& assignment = trail.Assignment();
743 for (int i = 0; i < size; ++i) {
744 if (assignment.LiteralIsFalse(literals[i])) continue;
745 tails[new_size] = tails[i];
746 heads[new_size] = heads[i];
747 literals[new_size] = literals[i];
748 ++new_size;
749 }
750 if (new_size < size) {
751 tails.resize(new_size);
752 heads.resize(new_size);
753 literals.resize(new_size);
754 }
755}
756
757} // namespace
758
759// We use a basic algorithm to detect components that are not connected to the
760// rest of the graph in the LP solution, and add cuts to force some arcs to
761// enter and leave this component from outside.
763 int num_nodes, std::vector<int> tails, std::vector<int> heads,
764 std::vector<Literal> literals, Model* model) {
765 CutGenerator result;
766 result.vars = GetAssociatedVariables(literals, model);
767 result.generate_cuts = [=](LinearConstraintManager* manager) mutable {
768 FilterFalseArcsAtLevelZero(tails, heads, literals, model);
769 SeparateSubtourInequalities(num_nodes, tails, heads, literals,
770 /*demands=*/{}, /*capacity=*/0, manager, model);
771 return true;
772 };
773 return result;
774}
775
776CutGenerator CreateCVRPCutGenerator(int num_nodes, std::vector<int> tails,
777 std::vector<int> heads,
778 std::vector<Literal> literals,
779 std::vector<int64_t> demands,
780 int64_t capacity, Model* model) {
781 CutGenerator result;
782 result.vars = GetAssociatedVariables(literals, model);
783 result.generate_cuts = [=](LinearConstraintManager* manager) mutable {
784 FilterFalseArcsAtLevelZero(tails, heads, literals, model);
785 SeparateSubtourInequalities(num_nodes, tails, heads, literals, demands,
786 capacity, manager, model);
787 return true;
788 };
789 return result;
790}
791
792// This is really similar to SeparateSubtourInequalities, see the reference
793// there.
795 int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
796 absl::Span<const AffineExpression> arc_capacities,
797 std::function<void(const std::vector<bool>& in_subset,
798 IntegerValue* min_incoming_flow,
799 IntegerValue* min_outgoing_flow)>
800 get_flows,
803 // We will collect only the arcs with a positive lp capacity value to speed up
804 // some computation below.
805 struct Arc {
806 int tail;
807 int head;
808 double lp_value;
809 IntegerValue offset;
810 };
811 std::vector<Arc> relevant_arcs;
812
813 // Often capacities have a coeff > 1.
814 // We currently exploit this if all coeff have a gcd > 1.
815 int64_t gcd = 0;
816
817 // Sort the arcs by non-increasing lp_values.
818 std::vector<std::pair<double, int>> arc_by_decreasing_lp_values;
819 for (int i = 0; i < arc_capacities.size(); ++i) {
820 const double lp_value = arc_capacities[i].LpValue(lp_values);
821 if (!arc_capacities[i].IsConstant()) {
822 gcd = MathUtil::GCD64(gcd, std::abs(arc_capacities[i].coeff.value()));
823 }
824 if (lp_value < 1e-6 && arc_capacities[i].constant == 0) continue;
825 relevant_arcs.push_back(
826 {tails[i], heads[i], lp_value, arc_capacities[i].constant});
827 arc_by_decreasing_lp_values.push_back({lp_value, i});
828 }
829 if (gcd == 0) return;
830 std::sort(arc_by_decreasing_lp_values.begin(),
831 arc_by_decreasing_lp_values.end(),
832 std::greater<std::pair<double, int>>());
833
834 std::vector<std::pair<int, int>> ordered_arcs;
835 for (const auto& [score, arc] : arc_by_decreasing_lp_values) {
836 if (tails[arc] == -1) continue;
837 if (heads[arc] == -1) continue;
838 ordered_arcs.push_back({tails[arc], heads[arc]});
839 }
840 std::vector<int> subset_data;
841 std::vector<absl::Span<const int>> subsets;
842 GenerateInterestingSubsets(num_nodes, ordered_arcs,
843 /*stop_at_num_components=*/1, &subset_data,
844 &subsets);
845
846 // Process each subsets and add any violated cut.
847 std::vector<bool> in_subset(num_nodes, false);
848 for (const absl::Span<const int> subset : subsets) {
849 DCHECK(!subset.empty());
850 DCHECK_LE(subset.size(), num_nodes);
851
852 // Initialize "in_subset" and the subset demands.
853 for (const int n : subset) in_subset[n] = true;
854
855 IntegerValue min_incoming_flow;
856 IntegerValue min_outgoing_flow;
857 get_flows(in_subset, &min_incoming_flow, &min_outgoing_flow);
858
859 // We will sum the offset of all incoming/outgoing arc capacities.
860 // Note that all arcs with a non-zero offset are part of relevant_arcs.
861 IntegerValue incoming_offset(0);
862 IntegerValue outgoing_offset(0);
863
864 // Compute the current flow in and out of the subset.
865 //
866 // This can take a significant portion of the running time, it is why it is
867 // faster to do it only on arcs with non-zero lp values which should be in
868 // linear number rather than the total number of arc which can be quadratic.
869 double lp_outgoing_flow = 0.0;
870 double lp_incoming_flow = 0.0;
871 for (const auto arc : relevant_arcs) {
872 if (arc.tail != -1 && in_subset[arc.tail]) {
873 if (arc.head == -1 || !in_subset[arc.head]) {
874 incoming_offset += arc.offset;
875 lp_outgoing_flow += arc.lp_value;
876 }
877 } else {
878 if (arc.head != -1 && in_subset[arc.head]) {
879 outgoing_offset += arc.offset;
880 lp_incoming_flow += arc.lp_value;
881 }
882 }
883 }
884
885 // If the gcd is greater than one, because all variables are integer we
886 // can round the flow lower bound to the next multiple of the gcd.
887 //
888 // TODO(user): Alternatively, try MIR heuristics if the coefficients in
889 // the capacities are not all the same.
890 if (gcd > 1) {
891 const IntegerValue test_incoming = min_incoming_flow - incoming_offset;
892 const IntegerValue new_incoming =
893 CeilRatio(test_incoming, IntegerValue(gcd)) * IntegerValue(gcd);
894 const IntegerValue incoming_delta = new_incoming - test_incoming;
895 if (incoming_delta > 0) min_incoming_flow += incoming_delta;
896 }
897 if (gcd > 1) {
898 const IntegerValue test_outgoing = min_outgoing_flow - outgoing_offset;
899 const IntegerValue new_outgoing =
900 CeilRatio(test_outgoing, IntegerValue(gcd)) * IntegerValue(gcd);
901 const IntegerValue outgoing_delta = new_outgoing - test_outgoing;
902 if (outgoing_delta > 0) min_outgoing_flow += outgoing_delta;
903 }
904
905 if (lp_incoming_flow < ToDouble(min_incoming_flow) - 1e-6) {
906 VLOG(2) << "INCOMING CUT " << lp_incoming_flow
907 << " >= " << min_incoming_flow << " size " << subset.size()
908 << " offset " << incoming_offset << " gcd " << gcd;
909 LinearConstraintBuilder cut(model, min_incoming_flow, kMaxIntegerValue);
910 for (int i = 0; i < tails.size(); ++i) {
911 if ((tails[i] == -1 || !in_subset[tails[i]]) &&
912 (heads[i] != -1 && in_subset[heads[i]])) {
913 cut.AddTerm(arc_capacities[i], 1.0);
914 }
915 }
916 manager->AddCut(cut.Build(), "IncomingFlow");
917 }
918
919 if (lp_outgoing_flow < ToDouble(min_outgoing_flow) - 1e-6) {
920 VLOG(2) << "OUGOING CUT " << lp_outgoing_flow
921 << " >= " << min_outgoing_flow << " size " << subset.size()
922 << " offset " << outgoing_offset << " gcd " << gcd;
923 LinearConstraintBuilder cut(model, min_outgoing_flow, kMaxIntegerValue);
924 for (int i = 0; i < tails.size(); ++i) {
925 if ((tails[i] != -1 && in_subset[tails[i]]) &&
926 (heads[i] == -1 || !in_subset[heads[i]])) {
927 cut.AddTerm(arc_capacities[i], 1.0);
928 }
929 }
930 manager->AddCut(cut.Build(), "OutgoingFlow");
931 }
932
933 // Sparse clean up.
934 for (const int n : subset) in_subset[n] = false;
935 }
936}
937
939 int num_nodes, const std::vector<int>& tails, const std::vector<int>& heads,
940 const std::vector<AffineExpression>& arc_capacities,
941 std::function<void(const std::vector<bool>& in_subset,
942 IntegerValue* min_incoming_flow,
943 IntegerValue* min_outgoing_flow)>
944 get_flows,
945 Model* model) {
946 CutGenerator result;
947 for (const AffineExpression expr : arc_capacities) {
948 if (!expr.IsConstant()) result.vars.push_back(expr.var);
949 }
950 result.generate_cuts = [=](LinearConstraintManager* manager) {
951 SeparateFlowInequalities(num_nodes, tails, heads, arc_capacities, get_flows,
952 manager->LpValues(), manager, model);
953 return true;
954 };
955 return result;
956}
957
958} // namespace sat
959} // namespace operations_research
IntegerValue size
static int64_t GCD64(int64_t x, int64_t y)
Definition mathutil.h:108
ArcIndex AddArcWithCapacity(NodeIndex tail, NodeIndex head, FlowQuantity capacity)
Definition max_flow.cc:32
Status Solve(NodeIndex source, NodeIndex sink)
Definition max_flow.cc:59
void GetSourceSideMinCut(std::vector< NodeIndex > *result)
Definition max_flow.cc:114
void AddTerm(IntegerVariable var, IntegerValue coeff)
bool AddCut(LinearConstraint ct, std::string type_name, std::string extra_info="")
const util_intops::StrongVector< IntegerVariable, double > & LpValues()
To simplify CutGenerator api.
int64_t a
Definition table.cc:44
Block * next
GraphType graph
const std::string name
A name for logging purposes.
GRBmodel * model
int arc
std::vector< int > ComputeGomoryHuTree(int num_nodes, const std::vector< ArcWithLpValue > &relevant_arcs)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
CutGenerator CreateCVRPCutGenerator(int num_nodes, std::vector< int > tails, std::vector< int > heads, std::vector< Literal > literals, std::vector< int64_t > demands, int64_t capacity, Model *model)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition integer.h:85
void SeparateSubtourInequalities(int num_nodes, const std::vector< int > &tails, const std::vector< int > &heads, const std::vector< Literal > &literals, absl::Span< const int64_t > demands, int64_t capacity, LinearConstraintManager *manager, Model *model)
IntType CeilOfRatio(IntType numerator, IntType denominator)
Definition util.h:729
CutGenerator CreateStronglyConnectedGraphCutGenerator(int num_nodes, std::vector< int > tails, std::vector< int > heads, std::vector< Literal > literals, Model *model)
const IntegerVariable kNoIntegerVariable(-1)
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)
void ExtractAllSubsetsFromForest(const std::vector< int > &parent, std::vector< int > *subset_data, std::vector< absl::Span< const int > > *subsets, int node_limit)
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 GenerateInterestingSubsets(int num_nodes, const std::vector< std::pair< int, int > > &arcs, int stop_at_num_components, std::vector< int > *subset_data, std::vector< absl::Span< const int > > *subsets)
void SymmetrizeArcs(std::vector< ArcWithLpValue > *arcs)
double ToDouble(IntegerValue value)
Definition integer.h:73
In SWIG mode, we don't want anything besides these top-level includes.
int64_t demand
Definition resource.cc:126
int head
int tail
int64_t start
std::vector< IntegerVariable > vars
Definition cuts.h:55
std::function< bool(LinearConstraintManager *manager)> generate_cuts
Definition cuts.h:56