OR-Tools  9.3
cp_model_symmetries.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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 <stddef.h>
17
18#include <algorithm>
19#include <cstdint>
20#include <limits>
21#include <memory>
22#include <utility>
23#include <vector>
24
25#include "absl/container/flat_hash_map.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/memory/memory.h"
28#include "absl/meta/type_traits.h"
29#include "absl/status/status.h"
30#include "absl/strings/str_cat.h"
31#include "absl/strings/str_join.h"
32#include "google/protobuf/message.h"
35#include "ortools/base/hash.h"
38#include "ortools/graph/graph.h"
39#include "ortools/sat/cp_model.pb.h"
42#include "ortools/sat/model.h"
45#include "ortools/sat/sat_parameters.pb.h"
51
52namespace operations_research {
53namespace sat {
54
55namespace {
56struct VectorHash {
57 std::size_t operator()(const std::vector<int64_t>& values) const {
58 size_t hash = 0;
59 for (const int64_t value : values) {
61 }
62 return hash;
63 }
64};
65
66// A simple class to generate equivalence class number for
67// GenerateGraphForSymmetryDetection().
68class IdGenerator {
69 public:
70 IdGenerator() {}
71
72 // If the color was never seen before, then generate a new id, otherwise
73 // return the previously generated id.
74 int GetId(const std::vector<int64_t>& color) {
75 return gtl::LookupOrInsert(&id_map_, color, id_map_.size());
76 }
77
78 int NextFreeId() const { return id_map_.size(); }
79
80 private:
81 absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
82};
83
84// Appends values in `repeated_field` to `vector`.
85//
86// We use a template as proto int64_t != C++ int64_t in open source.
87template <typename FieldInt64Type>
88void Append(
89 const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
90 std::vector<int64_t>* vector) {
91 CHECK(vector != nullptr);
92 for (const FieldInt64Type value : repeated_field) {
93 vector->push_back(value);
94 }
95}
96
97// Returns a graph whose automorphisms can be mapped back to the symmetries of
98// the model described in the given CpModelProto.
99//
100// Any permutation of the graph that respects the initial_equivalence_classes
101// output can be mapped to a symmetry of the given problem simply by taking its
102// restriction on the first num_variables nodes and interpreting its index as a
103// variable index. In a sense, a node with a low enough index #i is in
104// one-to-one correspondence with the variable #i (using the index
105// representation of variables).
106//
107// The format of the initial_equivalence_classes is the same as the one
108// described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
109// in [0, num_classes) and any symmetry will only map nodes with the same class
110// between each other.
111template <typename Graph>
112std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
113 const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
114 SolverLogger* logger) {
115 CHECK(initial_equivalence_classes != nullptr);
116
117 const int num_variables = problem.variables_size();
118 auto graph = absl::make_unique<Graph>();
119
120 // Each node will be created with a given color. Two nodes of different color
121 // can never be send one into another by a symmetry. The first element of
122 // the color vector will always be the NodeType.
123 //
124 // TODO(user): Using a full int64_t for storing 3 values is not great. We
125 // can optimize this at the price of a bit more code.
126 enum NodeType {
127 VARIABLE_NODE,
128 VAR_COEFFICIENT_NODE,
129 CONSTRAINT_NODE,
130 };
131 IdGenerator color_id_generator;
132 initial_equivalence_classes->clear();
133 auto new_node = [&initial_equivalence_classes, &graph,
134 &color_id_generator](const std::vector<int64_t>& color) {
135 // Since we add nodes one by one, initial_equivalence_classes->size() gives
136 // the number of nodes at any point, which we use as the next node index.
137 const int node = initial_equivalence_classes->size();
138 initial_equivalence_classes->push_back(color_id_generator.GetId(color));
139
140 // In some corner cases, we create a node but never uses it. We still
141 // want it to be there.
142 graph->AddNode(node);
143 return node;
144 };
145
146 // For two variables to be in the same equivalence class, they need to have
147 // the same objective coefficient, and the same possible bounds.
148 //
149 // TODO(user): We could ignore the objective coefficients, and just make sure
150 // that when we break symmetry amongst variables, we choose the possibility
151 // with the smallest cost?
152 std::vector<int64_t> objective_by_var(num_variables, 0);
153 for (int i = 0; i < problem.objective().vars_size(); ++i) {
154 const int ref = problem.objective().vars(i);
155 const int var = PositiveRef(ref);
156 const int64_t coeff = problem.objective().coeffs(i);
157 objective_by_var[var] = RefIsPositive(ref) ? coeff : -coeff;
158 }
159
160 // Create one node for each variable. Note that the code rely on the fact that
161 // the index of a VARIABLE_NODE type is the same as the variable index.
162 std::vector<int64_t> tmp_color;
163 for (int v = 0; v < num_variables; ++v) {
164 tmp_color = {VARIABLE_NODE, objective_by_var[v]};
165 Append(problem.variables(v).domain(), &tmp_color);
166 CHECK_EQ(v, new_node(tmp_color));
167 }
168
169 // We will lazily create "coefficient nodes" that correspond to a variable
170 // with a given coefficient.
171 absl::flat_hash_map<std::pair<int64_t, int64_t>, int> coefficient_nodes;
172 auto get_coefficient_node = [&new_node, &graph, &coefficient_nodes,
173 &tmp_color](int var, int64_t coeff) {
174 const int var_node = var;
176
177 // For a coefficient of one, which are the most common, we can optimize the
178 // size of the graph by omitting the coefficient node altogether and using
179 // directly the var_node in this case.
180 if (coeff == 1) return var_node;
181
182 const auto insert =
183 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
184 if (!insert.second) return insert.first->second;
185
186 tmp_color = {VAR_COEFFICIENT_NODE, coeff};
187 const int secondary_node = new_node(tmp_color);
188 graph->AddArc(var_node, secondary_node);
189 insert.first->second = secondary_node;
190 return secondary_node;
191 };
192
193 // For a literal we use the same as a coefficient 1 or -1. We can do that
194 // because literal and (var, coefficient) never appear together in the same
195 // constraint.
196 auto get_literal_node = [&get_coefficient_node](int ref) {
197 return get_coefficient_node(PositiveRef(ref), RefIsPositive(ref) ? 1 : -1);
198 };
199
200 // Because the implications can be numerous, we encode them without
201 // constraints node by using an arc from the lhs to the rhs. Note that we also
202 // always add the other direction. We use a set to remove duplicates both for
203 // efficiency and to not artificially break symmetries by using multi-arcs.
204 //
205 // Tricky: We cannot use the base variable node here to avoid situation like
206 // both a variable a and b having the same children (not(a), not(b)) in the
207 // graph. Because if that happen, we can permute a and b without permuting
208 // their associated not(a) and not(b) node! To be sure this cannot happen, a
209 // variable node can not have as children a VAR_COEFFICIENT_NODE from another
210 // node. This makes sure that any permutation that touch a variable, must
211 // permute its coefficient nodes accordingly.
212 absl::flat_hash_set<std::pair<int, int>> implications;
213 auto get_implication_node = [&new_node, &graph, &coefficient_nodes,
214 &tmp_color](int ref) {
215 const int var = PositiveRef(ref);
216 const int64_t coeff = RefIsPositive(ref) ? 1 : -1;
217 const auto insert =
218 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
219 if (!insert.second) return insert.first->second;
220 tmp_color = {VAR_COEFFICIENT_NODE, coeff};
221 const int secondary_node = new_node(tmp_color);
222 graph->AddArc(var, secondary_node);
223 insert.first->second = secondary_node;
224 return secondary_node;
225 };
226 auto add_implication = [&get_implication_node, &graph, &implications](
227 int ref_a, int ref_b) {
228 const auto insert = implications.insert({ref_a, ref_b});
229 if (!insert.second) return;
230 graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
231
232 // Always add the other side.
233 implications.insert({NegatedRef(ref_b), NegatedRef(ref_a)});
234 graph->AddArc(get_implication_node(NegatedRef(ref_b)),
235 get_implication_node(NegatedRef(ref_a)));
236 };
237
238 // We need to keep track of this for scheduling constraints.
239 absl::flat_hash_map<int, int> interval_constraint_index_to_node;
240
241 // Add constraints to the graph.
242 for (int constraint_index = 0; constraint_index < problem.constraints_size();
243 ++constraint_index) {
244 const ConstraintProto& constraint = problem.constraints(constraint_index);
245 const int constraint_node = initial_equivalence_classes->size();
246 std::vector<int64_t> color = {CONSTRAINT_NODE,
247 constraint.constraint_case()};
248
249 switch (constraint.constraint_case()) {
250 case ConstraintProto::CONSTRAINT_NOT_SET:
251 // TODO(user): We continue for the corner case of a constraint not set
252 // with enforcement literal. We should probably clear this constraint
253 // before reaching here.
254 continue;
255 case ConstraintProto::kLinear: {
256 // TODO(user): We can use the same trick as for the implications to
257 // encode relations of the form coeff * var_a <= coeff * var_b without
258 // creating a constraint node by directly adding an arc between the two
259 // var coefficient nodes.
260 Append(constraint.linear().domain(), &color);
261 CHECK_EQ(constraint_node, new_node(color));
262 for (int i = 0; i < constraint.linear().vars_size(); ++i) {
263 const int ref = constraint.linear().vars(i);
264 const int variable_node = PositiveRef(ref);
265 const int64_t coeff = RefIsPositive(ref)
266 ? constraint.linear().coeffs(i)
267 : -constraint.linear().coeffs(i);
268 graph->AddArc(get_coefficient_node(variable_node, coeff),
269 constraint_node);
270 }
271 break;
272 }
273 case ConstraintProto::kBoolOr: {
274 CHECK_EQ(constraint_node, new_node(color));
275 for (const int ref : constraint.bool_or().literals()) {
276 graph->AddArc(get_literal_node(ref), constraint_node);
277 }
278 break;
279 }
280 case ConstraintProto::kAtMostOne: {
281 if (constraint.at_most_one().literals().size() == 2) {
282 // Treat it as an implication to avoid creating a node.
283 add_implication(constraint.at_most_one().literals(0),
284 NegatedRef(constraint.at_most_one().literals(1)));
285 break;
286 }
287
288 CHECK_EQ(constraint_node, new_node(color));
289 for (const int ref : constraint.at_most_one().literals()) {
290 graph->AddArc(get_literal_node(ref), constraint_node);
291 }
292 break;
293 }
294 case ConstraintProto::kExactlyOne: {
295 CHECK_EQ(constraint_node, new_node(color));
296 for (const int ref : constraint.exactly_one().literals()) {
297 graph->AddArc(get_literal_node(ref), constraint_node);
298 }
299 break;
300 }
301 case ConstraintProto::kBoolXor: {
302 CHECK_EQ(constraint_node, new_node(color));
303 for (const int ref : constraint.bool_xor().literals()) {
304 graph->AddArc(get_literal_node(ref), constraint_node);
305 }
306 break;
307 }
308 case ConstraintProto::kBoolAnd: {
309 // The other cases should be presolved before this is called.
310 // TODO(user): not 100% true, this happen on rmatr200-p5, Fix.
311 if (constraint.enforcement_literal_size() != 1) {
313 logger,
314 "[Symmetry] BoolAnd with multiple enforcement literal are not "
315 "supported in symmetry code:",
316 constraint.ShortDebugString());
317 return nullptr;
318 }
319
320 CHECK_EQ(constraint.enforcement_literal_size(), 1);
321 const int ref_a = constraint.enforcement_literal(0);
322 for (const int ref_b : constraint.bool_and().literals()) {
323 add_implication(ref_a, ref_b);
324 }
325 break;
326 }
327 case ConstraintProto::kInterval: {
328 // We create 3 constraint nodes (for start, size and end) including the
329 // offset. We connect these to their terms like for a linear constraint.
330 std::vector<int> nodes;
331 for (int indicator = 0; indicator <= 2; ++indicator) {
332 const LinearExpressionProto& expr =
333 indicator == 0 ? constraint.interval().start()
334 : indicator == 1 ? constraint.interval().size()
335 : constraint.interval().end();
336
337 std::vector<int64_t> local_color = color;
338 local_color.push_back(indicator);
339 local_color.push_back(expr.offset());
340 const int local_node = new_node(local_color);
341 nodes.push_back(local_node);
342
343 for (int i = 0; i < expr.vars().size(); ++i) {
344 const int ref = expr.vars(i);
345 const int var_node = PositiveRef(ref);
346 const int64_t coeff =
347 RefIsPositive(ref) ? expr.coeffs(i) : -expr.coeffs(i);
348 graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
349 }
350 }
351
352 // We will only map enforcement literal to the start_node below because
353 // it has the same index as the constraint_node.
354 interval_constraint_index_to_node[constraint_index] = constraint_node;
355 CHECK_EQ(nodes[0], constraint_node);
356
357 // Make sure that if one node is mapped to another one, its other two
358 // components are the same.
359 graph->AddArc(nodes[0], nodes[1]);
360 graph->AddArc(nodes[1], nodes[2]);
361 graph->AddArc(nodes[2], nodes[0]); // TODO(user): not needed?
362 break;
363 }
364 case ConstraintProto::kNoOverlap: {
365 // Note(user): This require that intervals appear before they are used.
366 // We currently enforce this at validation, otherwise we need two passes
367 // here and in a bunch of other places.
368 CHECK_EQ(constraint_node, new_node(color));
369 for (const int interval : constraint.no_overlap().intervals()) {
370 graph->AddArc(interval_constraint_index_to_node.at(interval),
371 constraint_node);
372 }
373 break;
374 }
375 case ConstraintProto::kNoOverlap2D: {
376 // Note(user): This require that intervals appear before they are used.
377 // We currently enforce this at validation, otherwise we need two passes
378 // here and in a bunch of other places.
379 //
380 // TODO(user): With this graph encoding, we loose the symmetry that the
381 // dimension x can be swapped with the dimension y. I think it is
382 // possible to encode this by creating two extra nodes X and
383 // Y, each connected to all the x and all the y, but I have to think
384 // more about it.
385 CHECK_EQ(constraint_node, new_node(color));
386 const int size = constraint.no_overlap_2d().x_intervals().size();
387 for (int i = 0; i < size; ++i) {
388 const int x = constraint.no_overlap_2d().x_intervals(i);
389 const int y = constraint.no_overlap_2d().y_intervals(i);
390 graph->AddArc(interval_constraint_index_to_node.at(x),
391 constraint_node);
392 graph->AddArc(interval_constraint_index_to_node.at(x),
393 interval_constraint_index_to_node.at(y));
394 }
395 break;
396 }
397 default: {
398 // If the model contains any non-supported constraints, return an empty
399 // graph.
400 //
401 // TODO(user): support other types of constraints. Or at least, we
402 // could associate to them an unique node so that their variables can
403 // appear in no symmetry.
404 VLOG(1) << "Unsupported constraint type "
405 << ConstraintCaseName(constraint.constraint_case());
406 return nullptr;
407 }
408 }
409
410 // For enforcement, we use a similar trick than for the implications.
411 // Because all our constraint arcs are in the direction var_node to
412 // constraint_node, we just use the reverse direction for the enforcement
413 // part. This way we can reuse the same get_literal_node() function.
414 if (constraint.constraint_case() != ConstraintProto::kBoolAnd) {
415 for (const int ref : constraint.enforcement_literal()) {
416 graph->AddArc(constraint_node, get_literal_node(ref));
417 }
418 }
419 }
420
421 graph->Build();
422 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
423
424 // TODO(user): The symmetry code does not officially support multi-arcs. And
425 // we shouldn't have any as long as there is no duplicates variable in our
426 // constraints (but of course, we can't always guarantee that). That said,
427 // because the symmetry code really only look at the degree, it works as long
428 // as the maximum degree is bounded by num_nodes.
429 const int num_nodes = graph->num_nodes();
430 std::vector<int> in_degree(num_nodes, 0);
431 std::vector<int> out_degree(num_nodes, 0);
432 for (int i = 0; i < num_nodes; ++i) {
433 out_degree[i] = graph->OutDegree(i);
434 for (const int head : (*graph)[i]) {
435 in_degree[head]++;
436 }
437 }
438 for (int i = 0; i < num_nodes; ++i) {
439 if (in_degree[i] >= num_nodes || out_degree[i] >= num_nodes) {
440 SOLVER_LOG(logger, "[Symmetry] Too many multi-arcs in symmetry code.");
441 return nullptr;
442 }
443 }
444
445 // Because this code is running during presolve, a lot a variable might have
446 // no edges. We do not want to detect symmetries between these.
447 //
448 // Note that this code forces us to "densify" the ids afterwards because the
449 // symmetry detection code relies on that.
450 //
451 // TODO(user): It will probably be more efficient to not even create these
452 // nodes, but we will need a mapping to know the variable <-> node index.
453 int next_id = color_id_generator.NextFreeId();
454 for (int i = 0; i < num_variables; ++i) {
455 if ((*graph)[i].empty()) {
456 (*initial_equivalence_classes)[i] = next_id++;
457 }
458 }
459
460 // Densify ids.
461 int id = 0;
462 std::vector<int> mapping(next_id, -1);
463 for (int& ref : *initial_equivalence_classes) {
464 if (mapping[ref] == -1) {
465 ref = mapping[ref] = id++;
466 } else {
467 ref = mapping[ref];
468 }
469 }
470
471 return graph;
472}
473} // namespace
474
476 const SatParameters& params, const CpModelProto& problem,
477 std::vector<std::unique_ptr<SparsePermutation>>* generators,
478 double deterministic_limit, SolverLogger* logger) {
479 CHECK(generators != nullptr);
480 generators->clear();
481
482 if (params.symmetry_level() < 3 && problem.variables().size() > 1e6 &&
483 problem.constraints().size() > 1e6) {
484 SOLVER_LOG(logger,
485 "[Symmetry] Problem too large. Skipping. You can use "
486 "symmetry_level:3 or more to force it.");
487 return;
488 }
489
491
492 std::vector<int> equivalence_classes;
493 std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
494 problem, &equivalence_classes, logger));
495 if (graph == nullptr) return;
496
497 SOLVER_LOG(logger, "[Symmetry] Graph for symmetry has ", graph->num_nodes(),
498 " nodes and ", graph->num_arcs(), " arcs.");
499 if (graph->num_nodes() == 0) return;
500
501 GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/false);
502 std::vector<int> factorized_automorphism_group_size;
503 std::unique_ptr<TimeLimit> time_limit =
504 TimeLimit::FromDeterministicTime(deterministic_limit);
505 const absl::Status status = symmetry_finder.FindSymmetries(
506 &equivalence_classes, generators, &factorized_automorphism_group_size,
507 time_limit.get());
508
509 // TODO(user): Change the API to not return an error when the time limit is
510 // reached.
511 if (!status.ok()) {
512 SOLVER_LOG(logger,
513 "[Symmetry] GraphSymmetryFinder error: ", status.message());
514 }
515
516 // Remove from the permutations the part not concerning the variables.
517 // Note that some permutations may become empty, which means that we had
518 // duplicate constraints.
519 double average_support_size = 0.0;
520 int num_generators = 0;
521 int num_duplicate_constraints = 0;
522 for (int i = 0; i < generators->size(); ++i) {
523 SparsePermutation* permutation = (*generators)[i].get();
524 std::vector<int> to_delete;
525 for (int j = 0; j < permutation->NumCycles(); ++j) {
526 // Because variable nodes are in a separate equivalence class than any
527 // other node, a cycle can either contain only variable nodes or none, so
528 // we just need to check one element of the cycle.
529 if (*(permutation->Cycle(j).begin()) >= problem.variables_size()) {
530 to_delete.push_back(j);
531 if (DEBUG_MODE) {
532 // Verify that the cycle's entire support does not touch any variable.
533 for (const int node : permutation->Cycle(j)) {
534 DCHECK_GE(node, problem.variables_size());
535 }
536 }
537 }
538 }
539
540 permutation->RemoveCycles(to_delete);
541 if (!permutation->Support().empty()) {
542 average_support_size += permutation->Support().size();
543 swap((*generators)[num_generators], (*generators)[i]);
544 ++num_generators;
545 } else {
546 ++num_duplicate_constraints;
547 }
548 }
549 generators->resize(num_generators);
550 average_support_size /= num_generators;
551 SOLVER_LOG(logger, "[Symmetry] Symmetry computation done. time: ",
552 time_limit->GetElapsedTime(),
554 if (num_generators > 0) {
555 SOLVER_LOG(logger, "[Symmetry] #generators: ", num_generators,
556 ", average support size: ", average_support_size);
557 if (num_duplicate_constraints > 0) {
558 SOLVER_LOG(logger, "[Symmetry] The model contains ",
559 num_duplicate_constraints, " duplicate constraints !");
560 }
561 }
562}
563
564void DetectAndAddSymmetryToProto(const SatParameters& params,
565 CpModelProto* proto, SolverLogger* logger) {
566 SymmetryProto* symmetry = proto->mutable_symmetry();
567 symmetry->Clear();
568
569 std::vector<std::unique_ptr<SparsePermutation>> generators;
570 FindCpModelSymmetries(params, *proto, &generators,
571 /*deterministic_limit=*/1.0, logger);
572 if (generators.empty()) {
573 proto->clear_symmetry();
574 return;
575 }
576
577 for (const std::unique_ptr<SparsePermutation>& perm : generators) {
578 SparsePermutationProto* perm_proto = symmetry->add_permutations();
579 const int num_cycle = perm->NumCycles();
580 for (int i = 0; i < num_cycle; ++i) {
581 const int old_size = perm_proto->support().size();
582 for (const int var : perm->Cycle(i)) {
583 perm_proto->add_support(var);
584 }
585 perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
586 }
587 }
588
589 std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
590 if (orbitope.empty()) return;
591 SOLVER_LOG(logger, "[Symmetry] Found orbitope of size ", orbitope.size(),
592 " x ", orbitope[0].size());
593 DenseMatrixProto* matrix = symmetry->add_orbitopes();
594 matrix->set_num_rows(orbitope.size());
595 matrix->set_num_cols(orbitope[0].size());
596 for (const std::vector<int>& row : orbitope) {
597 for (const int entry : row) {
598 matrix->add_entries(entry);
599 }
600 }
601}
602
603namespace {
604
605// Given one Boolean orbit under symmetry, if there is a Boolean at one in this
606// orbit, then we can always move it to a fixed position (i.e. the given
607// variable var). Moreover, any variable implied to zero in this orbit by var
608// being at one can be fixed to zero. This is because, after symmetry breaking,
609// either var is one, or all the orbit is zero. We also add implications to
610// enforce this fact, but this is not done in this function.
611//
612// TODO(user): If an exactly one / at least one is included in the orbit, then
613// we can set a given variable to one directly. We can also detect this by
614// trying to propagate the orbit to all false.
615//
616// TODO(user): The same reasonning can be done if fixing the variable to
617// zero leads to many propagations at one. For general variables, we might be
618// able to do something too.
619void OrbitAndPropagation(const std::vector<int>& orbits, int var,
620 std::vector<int>* can_be_fixed_to_false,
621 PresolveContext* context) {
622 // Note that if a variable is fixed in the orbit, then everything should be
623 // fixed.
624 if (context->IsFixed(var)) return;
625 if (!context->CanBeUsedAsLiteral(var)) return;
626
627 // Lets fix var to true and see what is propagated.
628 //
629 // TODO(user): Ideally we should have a propagator ready for this. Right now
630 // we load the full model if we detected symmetries. We should really combine
631 // this with probing even though this is "breaking" the symmetry so it cannot
632 // be applied as generally as probing.
633 //
634 // TODO(user): Note that probing can also benefit from symmetry, since in
635 // each orbit, only one variable needs to be probed, and any conclusion can
636 // be duplicated to all the variables from an orbit! It is also why we just
637 // need to propagate one variable here.
638 Model model;
639 if (!LoadModelForProbing(context, &model)) return;
640
641 auto* sat_solver = model.GetOrCreate<SatSolver>();
642 auto* mapping = model.GetOrCreate<CpModelMapping>();
643 const Literal to_propagate = mapping->Literal(var);
644
645 const VariablesAssignment& assignment = sat_solver->Assignment();
646 if (assignment.LiteralIsAssigned(to_propagate)) return;
647 sat_solver->EnqueueDecisionAndBackjumpOnConflict(to_propagate);
648 if (sat_solver->CurrentDecisionLevel() != 1) return;
649
650 // We can fix to false any variable that is in the orbit and set to false!
651 can_be_fixed_to_false->clear();
652 int orbit_size = 0;
653 const int orbit_index = orbits[var];
654 const int num_variables = orbits.size();
655 for (int var = 0; var < num_variables; ++var) {
656 if (orbits[var] != orbit_index) continue;
657 ++orbit_size;
658
659 // By symmetry since same orbit.
660 DCHECK(!context->IsFixed(var));
661 DCHECK(context->CanBeUsedAsLiteral(var));
662
663 if (assignment.LiteralIsFalse(mapping->Literal(var))) {
664 can_be_fixed_to_false->push_back(var);
665 }
666 }
667 if (!can_be_fixed_to_false->empty()) {
668 SOLVER_LOG(context->logger(),
669 "[Symmetry] Num fixable by binary propagation in orbit: ",
670 can_be_fixed_to_false->size(), " / ", orbit_size);
671 }
672}
673
674} // namespace
675
677 const SatParameters& params = context->params();
678 const CpModelProto& proto = *context->working_model;
679
680 // We need to make sure the proto is up to date before computing symmetries!
681 if (context->working_model->has_objective()) {
682 context->WriteObjectiveToProto();
683 }
684 context->WriteVariableDomainsToProto();
685
686 // Tricky: the equivalence relation are not part of the proto.
687 // We thus add them temporarily to compute the symmetry.
688 int64_t num_added = 0;
689 const int initial_ct_index = proto.constraints().size();
690 const int num_vars = proto.variables_size();
691 for (int var = 0; var < num_vars; ++var) {
692 if (context->IsFixed(var)) continue;
693 if (context->VariableWasRemoved(var)) continue;
694 if (context->VariableIsNotUsedAnymore(var)) continue;
695
696 const AffineRelation::Relation r = context->GetAffineRelation(var);
697 if (r.representative == var) continue;
698
699 ++num_added;
700 ConstraintProto* ct = context->working_model->add_constraints();
701 auto* arg = ct->mutable_linear();
702 arg->add_vars(var);
703 arg->add_coeffs(1);
704 arg->add_vars(r.representative);
705 arg->add_coeffs(-r.coeff);
706 arg->add_domain(r.offset);
707 arg->add_domain(r.offset);
708 }
709
710 std::vector<std::unique_ptr<SparsePermutation>> generators;
711 FindCpModelSymmetries(params, proto, &generators,
712 /*deterministic_limit=*/1.0, context->logger());
713
714 // Remove temporary affine relation.
715 context->working_model->mutable_constraints()->DeleteSubrange(
716 initial_ct_index, num_added);
717
718 if (generators.empty()) return true;
719
720 // Collect the at most ones.
721 //
722 // Note(user): This relies on the fact that the pointers remain stable when
723 // we adds new constraints. It should be the case, but it is a bit unsafe.
724 // On the other hand it is annoying to deal with both cases below.
725 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
726 for (int i = 0; i < proto.constraints_size(); ++i) {
727 if (proto.constraints(i).constraint_case() == ConstraintProto::kAtMostOne) {
728 at_most_ones.push_back(&proto.constraints(i).at_most_one().literals());
729 }
730 if (proto.constraints(i).constraint_case() ==
731 ConstraintProto::kExactlyOne) {
732 at_most_ones.push_back(&proto.constraints(i).exactly_one().literals());
733 }
734 }
735
736 // We have a few heuristics. The firsts only look at the gobal orbits under
737 // the symmetry group and try to infer Boolean variable fixing via symmetry
738 // breaking. Note that nothing is fixed yet, we will decide later if we fix
739 // these Booleans or not.
740 int distinguished_var = -1;
741 std::vector<int> can_be_fixed_to_false;
742
743 // Get the global orbits and their size.
744 const std::vector<int> orbits = GetOrbits(num_vars, generators);
745 std::vector<int> orbit_sizes;
746 int max_orbit_size = 0;
747 for (int var = 0; var < num_vars; ++var) {
748 const int rep = orbits[var];
749 if (rep == -1) continue;
750 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
751 orbit_sizes[rep]++;
752 if (orbit_sizes[rep] > max_orbit_size) {
753 distinguished_var = var;
754 max_orbit_size = orbit_sizes[rep];
755 }
756 }
757
758 // Log orbit info.
759 if (context->logger()->LoggingIsEnabled()) {
760 std::vector<int> sorted_sizes;
761 for (const int s : orbit_sizes) {
762 if (s != 0) sorted_sizes.push_back(s);
763 }
764 std::sort(sorted_sizes.begin(), sorted_sizes.end(), std::greater<int>());
765 const int num_orbits = sorted_sizes.size();
766 if (num_orbits > 10) sorted_sizes.resize(10);
767 SOLVER_LOG(context->logger(), "[Symmetry] ", num_orbits,
768 " orbits with sizes: ", absl::StrJoin(sorted_sizes, ","),
769 (num_orbits > sorted_sizes.size() ? ",..." : ""));
770 }
771
772 // First heuristic based on propagation, see the function comment.
773 if (max_orbit_size > 2) {
774 OrbitAndPropagation(orbits, distinguished_var, &can_be_fixed_to_false,
775 context);
776 }
777 const int first_heuristic_size = can_be_fixed_to_false.size();
778
779 // If an at most one intersect with one or more orbit, in each intersection,
780 // we can fix all but one variable to zero. For now we only test positive
781 // literal, and maximize the number of fixing.
782 //
783 // TODO(user): Doing that is not always good, on cod105.mps, fixing variables
784 // instead of letting the innner solver handle Boolean symmetries make the
785 // problem unsolvable instead of easily solved. This is probably because this
786 // fixing do not exploit the full structure of these symmeteries. Note
787 // however that the fixing via propagation above close cod105 even more
788 // efficiently.
789 {
790 std::vector<int> tmp_to_clear;
791 std::vector<int> tmp_sizes(num_vars, 0);
792 for (const google::protobuf::RepeatedField<int32_t>* literals :
793 at_most_ones) {
794 tmp_to_clear.clear();
795
796 // Compute how many variables we can fix with this at most one.
797 int num_fixable = 0;
798 for (const int literal : *literals) {
799 if (!RefIsPositive(literal)) continue;
800 if (context->IsFixed(literal)) continue;
801
802 const int var = PositiveRef(literal);
803 const int rep = orbits[var];
804 if (rep == -1) continue;
805
806 // We count all but the first one in each orbit.
807 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
808 if (tmp_sizes[rep] > 0) ++num_fixable;
809 tmp_sizes[rep]++;
810 }
811
812 // Redo a pass to copy the intersection.
813 if (num_fixable > can_be_fixed_to_false.size()) {
814 distinguished_var = -1;
815 can_be_fixed_to_false.clear();
816 for (const int literal : *literals) {
817 if (!RefIsPositive(literal)) continue;
818 if (context->IsFixed(literal)) continue;
819
820 const int var = PositiveRef(literal);
821 const int rep = orbits[var];
822 if (rep == -1) continue;
823 if (distinguished_var == -1 ||
824 orbit_sizes[rep] > orbit_sizes[orbits[distinguished_var]]) {
825 distinguished_var = var;
826 }
827
828 // We push all but the first one in each orbit.
829 if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(var);
830 tmp_sizes[rep] = 0;
831 }
832 } else {
833 // Sparse clean up.
834 for (const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
835 }
836 }
837
838 if (can_be_fixed_to_false.size() > first_heuristic_size) {
840 context->logger(),
841 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
842 can_be_fixed_to_false.size(), " largest_orbit: ", max_orbit_size);
843 }
844 }
845
846 // Orbitope approach.
847 //
848 // This is basically the same as the generic approach, but because of the
849 // extra structure, computing the orbit of any stabilizer subgroup is easy.
850 // We look for orbits intersecting at most one constraints, so we can break
851 // symmetry by fixing variables.
852 //
853 // TODO(user): The same effect could be achieved by adding symmetry breaking
854 // constraints of the form "a >= b " between Booleans and let the presolve do
855 // the reduction. This might be less code, but it is also less efficient.
856 // Similarly, when we cannot just fix variables to break symmetries, we could
857 // add these constraints, but it is unclear if we should do it all the time or
858 // not.
859 //
860 // TODO(user): code the generic approach with orbits and stabilizer.
861 std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
862 if (!orbitope.empty()) {
863 SOLVER_LOG(context->logger(), "[Symmetry] Found orbitope of size ",
864 orbitope.size(), " x ", orbitope[0].size());
865 }
866
867 // Supper simple heuristic to use the orbitope or not.
868 //
869 // In an orbitope with an at most one on each row, we can fix the upper right
870 // triangle. We could use a formula, but the loop is fast enough.
871 //
872 // TODO(user): Compute the stabilizer under the only non-fixed element and
873 // iterate!
874 int max_num_fixed_in_orbitope = 0;
875 if (!orbitope.empty()) {
876 const int num_rows = orbitope[0].size();
877 int size_left = num_rows;
878 for (int col = 0; size_left > 1 && col < orbitope.size(); ++col) {
879 max_num_fixed_in_orbitope += size_left - 1;
880 --size_left;
881 }
882 }
883 if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
884 const int orbit_index = orbits[distinguished_var];
885 int num_in_orbit = 0;
886 for (int i = 0; i < can_be_fixed_to_false.size(); ++i) {
887 const int var = can_be_fixed_to_false[i];
888 if (orbits[var] == orbit_index) ++num_in_orbit;
889 context->UpdateRuleStats("symmetry: fixed to false in general orbit");
890 if (!context->SetLiteralToFalse(var)) return false;
891 }
892
893 // Moreover, we can add the implication that in the orbit of
894 // distinguished_var, either everything is false, or var is at one.
895 if (orbit_sizes[orbit_index] > num_in_orbit + 1) {
896 context->UpdateRuleStats(
897 "symmetry: added orbit symmetry breaking implications");
898 auto* ct = context->working_model->add_constraints();
899 auto* bool_and = ct->mutable_bool_and();
900 ct->add_enforcement_literal(NegatedRef(distinguished_var));
901 for (int var = 0; var < num_vars; ++var) {
902 if (orbits[var] != orbit_index) continue;
903 if (var == distinguished_var) continue;
904 if (context->IsFixed(var)) continue;
905 bool_and->add_literals(NegatedRef(var));
906 }
907 context->UpdateNewConstraintsVariableUsage();
908 }
909 return true;
910 }
911 if (orbitope.empty()) return true;
912
913 // This will always be kept all zero after usage.
914 std::vector<int> tmp_to_clear;
915 std::vector<int> tmp_sizes(num_vars, 0);
916 std::vector<int> tmp_num_positive(num_vars, 0);
917
918 // TODO(user): The code below requires that no variable appears twice in the
919 // same at most one. In particular lit and not(lit) cannot appear in the same
920 // at most one.
921 for (const google::protobuf::RepeatedField<int32_t>* literals :
922 at_most_ones) {
923 for (const int lit : *literals) {
924 const int var = PositiveRef(lit);
925 CHECK_NE(tmp_sizes[var], 1);
926 tmp_sizes[var] = 1;
927 }
928 for (const int lit : *literals) {
929 tmp_sizes[PositiveRef(lit)] = 0;
930 }
931 }
932
933 while (!orbitope.empty() && orbitope[0].size() > 1) {
934 const int num_cols = orbitope[0].size();
935 const std::vector<int> orbits = GetOrbitopeOrbits(num_vars, orbitope);
936
937 // Because in the orbitope case, we have a full symmetry group of the
938 // columns, we can infer more than just using the orbits under a general
939 // permutation group. If an at most one contains two variables from the
940 // orbit, we can infer:
941 // 1/ If the two variables appear positively, then there is an at most one
942 // on the full orbit, and we can set n - 1 variables to zero to break the
943 // symmetry.
944 // 2/ If the two variables appear negatively, then the opposite situation
945 // arise and there is at most one zero on the orbit, we can set n - 1
946 // variables to one.
947 // 3/ If two literals of opposite sign appear, then the only possibility
948 // for the orbit are all at one or all at zero, thus we can mark all
949 // variables as equivalent.
950 //
951 // These property comes from the fact that when we permute a line of the
952 // orbitope in any way, then the position than ends up in the at most one
953 // must never be both at one.
954 //
955 // Note that 1/ can be done without breaking any symmetry, but for 2/ and 3/
956 // by choosing which variable is not fixed, we will break some symmetry, and
957 // we will need to update the orbitope to stabilize this choice before
958 // continuing.
959 //
960 // TODO(user): for 2/ and 3/ we could add an at most one constraint on the
961 // full orbit if it is not already there!
962 //
963 // Note(user): On the miplib, only 1/ happens currently. Not sure with LNS
964 // though.
965 std::vector<bool> all_equivalent_rows(orbitope.size(), false);
966
967 // The result described above can be generalized if an at most one intersect
968 // many of the orbitope rows, each in at leat two positions. We will track
969 // the set of best rows on which we have an at most one (or at most one
970 // zero) on all their entries.
971 bool at_most_one_in_best_rows; // The alternative is at most one zero.
972 int64_t best_score = 0;
973 std::vector<int> best_rows;
974
975 std::vector<int> rows_in_at_most_one;
976 for (const google::protobuf::RepeatedField<int32_t>* literals :
977 at_most_ones) {
978 tmp_to_clear.clear();
979 for (const int literal : *literals) {
980 if (context->IsFixed(literal)) continue;
981 const int var = PositiveRef(literal);
982 const int rep = orbits[var];
983 if (rep == -1) continue;
984
985 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
986 tmp_sizes[rep]++;
987 if (RefIsPositive(literal)) tmp_num_positive[rep]++;
988 }
989
990 int num_positive_direction = 0;
991 int num_negative_direction = 0;
992
993 // An at most one touching two positions in an orbitope row can possibly
994 // be extended, depending if it has singleton intersection swith other
995 // rows and where.
996 bool possible_extension = false;
997
998 rows_in_at_most_one.clear();
999 for (const int row : tmp_to_clear) {
1000 const int size = tmp_sizes[row];
1001 const int num_positive = tmp_num_positive[row];
1002 const int num_negative = tmp_sizes[row] - tmp_num_positive[row];
1003 tmp_sizes[row] = 0;
1004 tmp_num_positive[row] = 0;
1005
1006 if (num_positive > 1 && num_negative == 0) {
1007 if (size < num_cols) possible_extension = true;
1008 rows_in_at_most_one.push_back(row);
1009 ++num_positive_direction;
1010 } else if (num_positive == 0 && num_negative > 1) {
1011 if (size < num_cols) possible_extension = true;
1012 rows_in_at_most_one.push_back(row);
1013 ++num_negative_direction;
1014 } else if (num_positive > 0 && num_negative > 0) {
1015 all_equivalent_rows[row] = true;
1016 }
1017 }
1018
1019 if (possible_extension) {
1020 context->UpdateRuleStats(
1021 "TODO symmetry: possible at most one extension.");
1022 }
1023
1024 if (num_positive_direction > 0 && num_negative_direction > 0) {
1025 return context->NotifyThatModelIsUnsat("Symmetry and at most ones");
1026 }
1027 const bool direction = num_positive_direction > 0;
1028
1029 // Because of symmetry, the choice of the column shouldn't matter (they
1030 // will all appear in the same number of constraints of the same types),
1031 // however we prefer to fix the variables that seems to touch more
1032 // constraints.
1033 //
1034 // TODO(user): maybe we should simplify the constraint using the variable
1035 // we fix before choosing the next row to break symmetry on. If there are
1036 // multiple row involved, we could also take the intersection instead of
1037 // probably counting the same constraints more than once.
1038 int64_t score = 0;
1039 for (const int row : rows_in_at_most_one) {
1040 score +=
1041 context->VarToConstraints(PositiveRef(orbitope[row][0])).size();
1042 }
1043 if (score > best_score) {
1044 at_most_one_in_best_rows = direction;
1045 best_score = score;
1046 best_rows = rows_in_at_most_one;
1047 }
1048 }
1049
1050 // Mark all the equivalence.
1051 // Note that this operation do not change the symmetry group.
1052 //
1053 // TODO(user): We could remove these rows from the orbitope. Note that
1054 // currently this never happen on the miplib (maybe in LNS though).
1055 for (int i = 0; i < all_equivalent_rows.size(); ++i) {
1056 if (all_equivalent_rows[i]) {
1057 for (int j = 1; j < num_cols; ++j) {
1058 context->StoreBooleanEqualityRelation(orbitope[i][0], orbitope[i][j]);
1059 context->UpdateRuleStats("symmetry: all equivalent in orbit");
1060 if (context->ModelIsUnsat()) return false;
1061 }
1062 }
1063 }
1064
1065 // Break the symmetry on our set of best rows by picking one columns
1066 // and setting all the other entries to zero or one. Note that the at most
1067 // one applies to all entries in all rows.
1068 //
1069 // TODO(user): We don't have any at most one relation on this orbitope,
1070 // but we could still add symmetry breaking inequality by picking any matrix
1071 // entry and making it the largest/lowest value on its row. This also work
1072 // for non-Booleans.
1073 if (best_score == 0) {
1074 context->UpdateRuleStats(
1075 "TODO symmetry: add symmetry breaking inequalities?");
1076 break;
1077 }
1078
1079 // If our symmetry group is valid, they cannot be any variable already
1080 // fixed to one (or zero if !at_most_one_in_best_rows). Otherwise all would
1081 // be fixed to one and the problem would be unsat.
1082 for (const int i : best_rows) {
1083 for (int j = 0; j < num_cols; ++j) {
1084 const int var = orbitope[i][j];
1085 if ((at_most_one_in_best_rows && context->LiteralIsTrue(var)) ||
1086 (!at_most_one_in_best_rows && context->LiteralIsFalse(var))) {
1087 return context->NotifyThatModelIsUnsat("Symmetry and at most one");
1088 }
1089 }
1090 }
1091
1092 // We have an at most one on a set of rows, we will pick a column, and set
1093 // all other entries on these rows to zero.
1094 //
1095 // TODO(user): All choices should be equivalent, but double check?
1096 const int best_col = 0;
1097 for (const int i : best_rows) {
1098 for (int j = 0; j < num_cols; ++j) {
1099 if (j == best_col) continue;
1100 const int var = orbitope[i][j];
1101 if (at_most_one_in_best_rows) {
1102 context->UpdateRuleStats("symmetry: fixed to false");
1103 if (!context->SetLiteralToFalse(var)) return false;
1104 } else {
1105 context->UpdateRuleStats("symmetry: fixed to true");
1106 if (!context->SetLiteralToTrue(var)) return false;
1107 }
1108 }
1109 }
1110
1111 // Remove all best rows.
1112 for (const int i : best_rows) orbitope[i].clear();
1113 int new_size = 0;
1114 for (int i = 0; i < orbitope.size(); ++i) {
1115 if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i];
1116 }
1117 CHECK_LT(new_size, orbitope.size());
1118 orbitope.resize(new_size);
1119
1120 // Remove best_col.
1121 for (int i = 0; i < orbitope.size(); ++i) {
1122 std::swap(orbitope[i][best_col], orbitope[i].back());
1123 orbitope[i].pop_back();
1124 }
1125 }
1126
1127 // If we are left with a set of variable than can all be permuted, lets
1128 // break the symmetry by ordering them.
1129 if (orbitope.size() == 1) {
1130 const int num_cols = orbitope[0].size();
1131 for (int i = 0; i + 1 < num_cols; ++i) {
1132 // Add orbitope[0][i] >= orbitope[0][i+1].
1133 ConstraintProto* ct = context->working_model->add_constraints();
1134 ct->mutable_linear()->add_coeffs(1);
1135 ct->mutable_linear()->add_vars(orbitope[0][i]);
1136 ct->mutable_linear()->add_coeffs(-1);
1137 ct->mutable_linear()->add_vars(orbitope[0][i + 1]);
1138 ct->mutable_linear()->add_domain(0);
1139 ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
1140 context->UpdateRuleStats("symmetry: added symmetry breaking inequality");
1141 }
1142 context->UpdateNewConstraintsVariableUsage();
1143 }
1144
1145 return true;
1146}
1147
1148} // namespace sat
1149} // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_LT(val1, val2)
Definition: base/logging.h:706
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:703
#define DCHECK_GE(val1, val2)
Definition: base/logging.h:895
#define CHECK_NE(val1, val2)
Definition: base/logging.h:704
#define DCHECK(condition)
Definition: base/logging.h:890
#define DCHECK_EQ(val1, val2)
Definition: base/logging.h:891
#define VLOG(verboselevel)
Definition: base/logging.h:984
absl::Status FindSymmetries(std::vector< int > *node_equivalence_classes_io, std::vector< std::unique_ptr< SparsePermutation > > *generators, std::vector< int > *factorized_automorphism_group_size, TimeLimit *time_limit=nullptr)
double GetElapsedDeterministicTime() const
Definition: time_limit.h:397
const std::vector< int > & Support() const
void RemoveCycles(const std::vector< int > &cycle_indices)
static std::unique_ptr< TimeLimit > FromDeterministicTime(double deterministic_limit)
Creates a time limit object that puts limit only on the deterministic time.
Definition: time_limit.h:145
CpModelProto proto
ModelSharedTimeLimit * time_limit
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
absl::Status status
Definition: g_gurobi.cc:35
GRBmodel * model
GurobiMPCallbackContext * context
const bool DEBUG_MODE
Definition: macros.h:24
ColIndex col
Definition: markowitz.cc:183
RowIndex row
Definition: markowitz.cc:182
int64_t hash
Definition: matrix_utils.cc:61
Collection::value_type::second_type & LookupOrInsert(Collection *const collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
Definition: map_util.h:237
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:262
void DetectAndAddSymmetryToProto(const SatParameters &params, CpModelProto *proto, SolverLogger *logger)
bool DetectAndExploitSymmetriesInPresolve(PresolveContext *context)
std::vector< int > GetOrbitopeOrbits(int n, const std::vector< std::vector< int > > &orbitope)
bool RefIsPositive(int ref)
bool LoadModelForProbing(PresolveContext *context, Model *local_model)
void FindCpModelSymmetries(const SatParameters &params, const CpModelProto &problem, std::vector< std::unique_ptr< SparsePermutation > > *generators, double deterministic_limit, SolverLogger *logger)
std::string ConstraintCaseName(ConstraintProto::ConstraintCase constraint_case)
std::vector< int > GetOrbits(int n, const std::vector< std::unique_ptr< SparsePermutation > > &generators)
std::vector< std::vector< int > > BasicOrbitopeExtraction(const std::vector< std::unique_ptr< SparsePermutation > > &generators)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
Collection of objects used to extend the Constraint Solver library.
uint64_t Hash(uint64_t num, uint64_t c)
Definition: hash.h:150
ListGraph Graph
Definition: graph.h:2362
Literal literal
Definition: optimization.cc:89
IntervalVar * interval
Definition: resource.cc:100
int64_t head
int nodes
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)
Definition: util/logging.h:69
const double coeff