OR-Tools  9.0
find_graph_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 <algorithm>
17 #include <cstdint>
18 #include <limits>
19 #include <numeric>
20 
21 #include "absl/memory/memory.h"
22 #include "absl/status/status.h"
23 #include "absl/strings/str_format.h"
24 #include "absl/strings/str_join.h"
25 #include "absl/time/clock.h"
26 #include "absl/time/time.h"
33 #include "ortools/graph/util.h"
34 
35 ABSL_FLAG(bool, minimize_permutation_support_size, false,
36  "Tweak the algorithm to try and minimize the support size"
37  " of the generators produced. This may negatively impact the"
38  " performance, but works great on the sat_holeXXX benchmarks"
39  " to reduce the support size.");
40 
41 namespace operations_research {
42 
44 
45 namespace {
46 // Some routines used below.
47 void SwapFrontAndBack(std::vector<int>* v) {
48  DCHECK(!v->empty());
49  std::swap((*v)[0], v->back());
50 }
51 
52 bool PartitionsAreCompatibleAfterPartIndex(const DynamicPartition& p1,
53  const DynamicPartition& p2,
54  int part_index) {
55  const int num_parts = p1.NumParts();
56  if (p2.NumParts() != num_parts) return false;
57  for (int p = part_index; p < num_parts; ++p) {
58  if (p1.SizeOfPart(p) != p2.SizeOfPart(p) ||
59  p1.ParentOfPart(p) != p2.ParentOfPart(p)) {
60  return false;
61  }
62  }
63  return true;
64 }
65 
66 // Whether the "l1" list maps to "l2" under the permutation "permutation".
67 // This method uses a transient bitmask on all the elements, which
68 // should be entirely false before the call (and will be restored as such
69 // after it).
70 //
71 // TODO(user): Make this method support multi-elements (i.e. an element may
72 // be repeated in the list), and see if that's sufficient to make the whole
73 // graph symmetry finder support multi-arcs.
74 template <class List>
75 bool ListMapsToList(const List& l1, const List& l2,
76  const DynamicPermutation& permutation,
77  std::vector<bool>* tmp_node_mask) {
78  int num_elements_delta = 0;
79  bool match = true;
80  for (const int mapped_x : l2) {
81  ++num_elements_delta;
82  (*tmp_node_mask)[mapped_x] = true;
83  }
84  for (const int x : l1) {
85  --num_elements_delta;
86  const int mapped_x = permutation.ImageOf(x);
87  if (!(*tmp_node_mask)[mapped_x]) {
88  match = false;
89  break;
90  }
91  (*tmp_node_mask)[mapped_x] = false;
92  }
93  if (num_elements_delta != 0) match = false;
94  if (!match) {
95  // We need to clean up tmp_node_mask.
96  for (const int x : l2) (*tmp_node_mask)[x] = false;
97  }
98  return match;
99 }
100 } // namespace
101 
102 GraphSymmetryFinder::GraphSymmetryFinder(const Graph& graph, bool is_undirected)
103  : graph_(graph),
104  tmp_dynamic_permutation_(NumNodes()),
105  tmp_node_mask_(NumNodes(), false),
106  tmp_degree_(NumNodes(), 0),
107  tmp_nodes_with_degree_(NumNodes() + 1) {
108  // Set up an "unlimited" time limit by default.
109  time_limit_ = &dummy_time_limit_;
110  tmp_partition_.Reset(NumNodes());
111  if (is_undirected) {
112  DCHECK(GraphIsSymmetric(graph));
113  } else {
114  // Compute the reverse adjacency lists.
115  // First pass: compute the total in-degree of all nodes and put it in
116  // reverse_adj_list_index (shifted by two; see below why).
117  reverse_adj_list_index_.assign(graph.num_nodes() + /*shift*/ 2, 0);
118  for (const int node : graph.AllNodes()) {
119  for (const int arc : graph.OutgoingArcs(node)) {
120  ++reverse_adj_list_index_[graph.Head(arc) + /*shift*/ 2];
121  }
122  }
123  // Second pass: apply a cumulative sum over reverse_adj_list_index.
124  // After that, reverse_adj_list contains:
125  // [0, 0, in_degree(node0), in_degree(node0) + in_degree(node1), ...]
126  std::partial_sum(reverse_adj_list_index_.begin() + /*shift*/ 2,
127  reverse_adj_list_index_.end(),
128  reverse_adj_list_index_.begin() + /*shift*/ 2);
129  // Third pass: populate "flattened_reverse_adj_lists", using
130  // reverse_adj_list_index[i] as a dynamic pointer to the yet-unpopulated
131  // area of the reverse adjacency list of node #i.
132  flattened_reverse_adj_lists_.assign(graph.num_arcs(), -1);
133  for (const int node : graph.AllNodes()) {
134  for (const int arc : graph.OutgoingArcs(node)) {
135  flattened_reverse_adj_lists_[reverse_adj_list_index_[graph.Head(arc) +
136  /*shift*/ 1]++] =
137  node;
138  }
139  }
140  // The last pass shifted reverse_adj_list_index, so it's now as we want it:
141  // [0, in_degree(node0), in_degree(node0) + in_degree(node1), ...]
142  if (DEBUG_MODE) {
143  DCHECK_EQ(graph.num_arcs(), reverse_adj_list_index_[graph.num_nodes()]);
144  for (const int i : flattened_reverse_adj_lists_) DCHECK_NE(i, -1);
145  }
146  }
147 }
148 
150  const DynamicPermutation& permutation) const {
151  for (const int base : permutation.AllMappingsSrc()) {
152  const int image = permutation.ImageOf(base);
153  if (image == base) continue;
154  if (!ListMapsToList(graph_[base], graph_[image], permutation,
155  &tmp_node_mask_)) {
156  return false;
157  }
158  }
159  if (!reverse_adj_list_index_.empty()) {
160  // The graph was not symmetric: we must also check the incoming arcs
161  // to displaced nodes.
162  for (const int base : permutation.AllMappingsSrc()) {
163  const int image = permutation.ImageOf(base);
164  if (image == base) continue;
165  if (!ListMapsToList(TailsOfIncomingArcsTo(base),
166  TailsOfIncomingArcsTo(image), permutation,
167  &tmp_node_mask_)) {
168  return false;
169  }
170  }
171  }
172  return true;
173 }
174 
175 namespace {
176 // Specialized subroutine, to avoid code duplication: see its call site
177 // and its self-explanatory code.
178 template <class T>
179 inline void IncrementCounterForNonSingletons(const T& nodes,
180  const DynamicPartition& partition,
181  std::vector<int>* node_count,
182  std::vector<int>* nodes_seen,
183  int64_t* num_operations) {
184  *num_operations += nodes.end() - nodes.begin();
185  for (const int node : nodes) {
186  if (partition.ElementsInSamePartAs(node).size() == 1) continue;
187  const int count = ++(*node_count)[node];
188  if (count == 1) nodes_seen->push_back(node);
189  }
190 }
191 } // namespace
192 
194  int first_unrefined_part_index, DynamicPartition* partition) {
195  // Rename, for readability of the code below.
196  std::vector<int>& tmp_nodes_with_nonzero_degree = tmp_stack_;
197 
198  // This function is the main bottleneck of the whole algorithm. We count the
199  // number of blocks in the inner-most loops in num_operations. At the end we
200  // will multiply it by a factor to have some deterministic time that we will
201  // append to the deterministic time counter.
202  //
203  // TODO(user): We are really imprecise in our counting, but it is fine. We
204  // just need a way to enforce a deterministic limit on the computation effort.
205  int64_t num_operations = 0;
206 
207  // Assuming that the partition was refined based on the adjacency on
208  // parts [0 .. first_unrefined_part_index) already, we simply need to
209  // refine parts first_unrefined_part_index ... NumParts()-1, the latter bound
210  // being a moving target:
211  // When a part #p < first_unrefined_part_index gets modified, it's always
212  // split in two: itself, and a new part #p'. Since #p was already refined
213  // on, we only need to further refine on *one* of its two split parts.
214  // And this will be done because p' > first_unrefined_part_index.
215  //
216  // Thus, the following loop really does the full recursive refinement as
217  // advertised.
218  std::vector<bool> adjacency_directions(1, /*outgoing*/ true);
219  if (!reverse_adj_list_index_.empty()) {
220  adjacency_directions.push_back(false); // Also look at incoming arcs.
221  }
222  for (int part_index = first_unrefined_part_index;
223  part_index < partition->NumParts(); // Moving target!
224  ++part_index) {
225  for (const bool outgoing_adjacency : adjacency_directions) {
226  // Count the aggregated degree of all nodes, only looking at arcs that
227  // come from/to the current part.
228  if (outgoing_adjacency) {
229  for (const int node : partition->ElementsInPart(part_index)) {
230  IncrementCounterForNonSingletons(
231  graph_[node], *partition, &tmp_degree_,
232  &tmp_nodes_with_nonzero_degree, &num_operations);
233  }
234  } else {
235  for (const int node : partition->ElementsInPart(part_index)) {
236  IncrementCounterForNonSingletons(
237  TailsOfIncomingArcsTo(node), *partition, &tmp_degree_,
238  &tmp_nodes_with_nonzero_degree, &num_operations);
239  }
240  }
241  // Group the nodes by (nonzero) degree. Remember the maximum degree.
242  int max_degree = 0;
243  num_operations += 3 + tmp_nodes_with_nonzero_degree.size();
244  for (const int node : tmp_nodes_with_nonzero_degree) {
245  const int degree = tmp_degree_[node];
246  tmp_degree_[node] = 0; // To clean up after us.
247  max_degree = std::max(max_degree, degree);
248  tmp_nodes_with_degree_[degree].push_back(node);
249  }
250  tmp_nodes_with_nonzero_degree.clear(); // To clean up after us.
251  // For each degree, refine the partition by the set of nodes with that
252  // degree.
253  for (int degree = 1; degree <= max_degree; ++degree) {
254  // We use a manually tuned factor 3 because Refine() does quite a bit of
255  // operations for each node in its argument.
256  num_operations += 1 + 3 * tmp_nodes_with_degree_[degree].size();
257  partition->Refine(tmp_nodes_with_degree_[degree]);
258  tmp_nodes_with_degree_[degree].clear(); // To clean up after us.
259  }
260  }
261  }
262 
263  // The coefficient was manually tuned (only on a few instances) so that the
264  // time is roughly correlated with seconds on a fast desktop computer from
265  // 2020.
266  time_limit_->AdvanceDeterministicTime(1e-8 *
267  static_cast<double>(num_operations));
268 }
269 
271  int node, DynamicPartition* partition, std::vector<int>* new_singletons) {
272  const int original_num_parts = partition->NumParts();
273  partition->Refine(std::vector<int>(1, node));
274  RecursivelyRefinePartitionByAdjacency(partition->PartOf(node), partition);
275 
276  // Explore the newly refined parts to gather all the new singletons.
277  if (new_singletons != nullptr) {
278  new_singletons->clear();
279  for (int p = original_num_parts; p < partition->NumParts(); ++p) {
280  const int parent = partition->ParentOfPart(p);
281  // We may see the same singleton parent several times, so we guard them
282  // with the tmp_node_mask_ boolean vector.
283  if (!tmp_node_mask_[parent] && parent < original_num_parts &&
284  partition->SizeOfPart(parent) == 1) {
285  tmp_node_mask_[parent] = true;
286  new_singletons->push_back(*partition->ElementsInPart(parent).begin());
287  }
288  if (partition->SizeOfPart(p) == 1) {
289  new_singletons->push_back(*partition->ElementsInPart(p).begin());
290  }
291  }
292  // Reset tmp_node_mask_.
293  for (int p = original_num_parts; p < partition->NumParts(); ++p) {
294  tmp_node_mask_[partition->ParentOfPart(p)] = false;
295  }
296  }
297 }
298 
299 namespace {
300 void MergeNodeEquivalenceClassesAccordingToPermutation(
301  const SparsePermutation& perm, MergingPartition* node_equivalence_classes,
302  DenseDoublyLinkedList* sorted_representatives) {
303  for (int c = 0; c < perm.NumCycles(); ++c) {
304  // TODO(user): use the global element->image iterator when it exists.
305  int prev = -1;
306  for (const int e : perm.Cycle(c)) {
307  if (prev >= 0) {
308  const int removed_representative =
309  node_equivalence_classes->MergePartsOf(prev, e);
310  if (sorted_representatives != nullptr && removed_representative != -1) {
311  sorted_representatives->Remove(removed_representative);
312  }
313  }
314  prev = e;
315  }
316  }
317 }
318 
319 // Subroutine used by FindSymmetries(); see its call site. This finds and
320 // outputs (in "pruned_other_nodes") the list of all representatives (under
321 // "node_equivalence_classes") that are in the same part as
322 // "representative_node" in "partition"; other than "representative_node"
323 // itself.
324 // "node_equivalence_classes" must be compatible with "partition", i.e. two
325 // nodes that are in the same equivalence class must also be in the same part.
326 //
327 // To do this in O(output size), we also need the
328 // "representatives_sorted_by_index_in_partition" data structure: the
329 // representatives of the nodes of the targeted part are contiguous in that
330 // linked list.
331 void GetAllOtherRepresentativesInSamePartAs(
332  int representative_node, const DynamicPartition& partition,
333  const DenseDoublyLinkedList& representatives_sorted_by_index_in_partition,
334  MergingPartition* node_equivalence_classes, // Only for debugging.
335  std::vector<int>* pruned_other_nodes) {
336  pruned_other_nodes->clear();
337  const int part_index = partition.PartOf(representative_node);
338  // Iterate on all contiguous representatives after the initial one...
339  int repr = representative_node;
340  while (true) {
341  DCHECK_EQ(repr, node_equivalence_classes->GetRoot(repr));
342  repr = representatives_sorted_by_index_in_partition.Prev(repr);
343  if (repr < 0 || partition.PartOf(repr) != part_index) break;
344  pruned_other_nodes->push_back(repr);
345  }
346  // ... and then on all contiguous representatives *before* it.
347  repr = representative_node;
348  while (true) {
349  DCHECK_EQ(repr, node_equivalence_classes->GetRoot(repr));
350  repr = representatives_sorted_by_index_in_partition.Next(repr);
351  if (repr < 0 || partition.PartOf(repr) != part_index) break;
352  pruned_other_nodes->push_back(repr);
353  }
354 
355  // This code is a bit tricky, so we check that we're doing it right, by
356  // comparing its output to the brute-force, O(Part size) version.
357  // This also (partly) verifies that
358  // "representatives_sorted_by_index_in_partition" is what it claims it is.
359  if (DEBUG_MODE) {
360  std::vector<int> expected_output;
361  for (const int e : partition.ElementsInPart(part_index)) {
362  if (node_equivalence_classes->GetRoot(e) != representative_node) {
363  expected_output.push_back(e);
364  }
365  }
366  node_equivalence_classes->KeepOnlyOneNodePerPart(&expected_output);
367  for (int& x : expected_output) x = node_equivalence_classes->GetRoot(x);
368  std::sort(expected_output.begin(), expected_output.end());
369  std::vector<int> sorted_output = *pruned_other_nodes;
370  std::sort(sorted_output.begin(), sorted_output.end());
371  DCHECK_EQ(absl::StrJoin(expected_output, " "),
372  absl::StrJoin(sorted_output, " "));
373  }
374 }
375 } // namespace
376 
378  std::vector<int>* node_equivalence_classes_io,
379  std::vector<std::unique_ptr<SparsePermutation>>* generators,
380  std::vector<int>* factorized_automorphism_group_size,
382  // Initialization.
383  time_limit_ = time_limit == nullptr ? &dummy_time_limit_ : time_limit;
384  IF_STATS_ENABLED(stats_.initialization_time.StartTimer());
385  generators->clear();
386  factorized_automorphism_group_size->clear();
387  if (node_equivalence_classes_io->size() != NumNodes()) {
388  return absl::Status(absl::StatusCode::kInvalidArgument,
389  "Invalid 'node_equivalence_classes_io'.");
390  }
391  DynamicPartition base_partition(*node_equivalence_classes_io);
392  // Break all inherent asymmetries in the graph.
393  {
394  ScopedTimeDistributionUpdater u(&stats_.initialization_refine_time);
395  RecursivelyRefinePartitionByAdjacency(/*first_unrefined_part_index=*/0,
396  &base_partition);
397  }
398  if (time_limit_->LimitReached()) {
399  return absl::Status(absl::StatusCode::kDeadlineExceeded,
400  "During the initial refinement.");
401  }
402  VLOG(4) << "Base partition: "
403  << base_partition.DebugString(DynamicPartition::SORT_BY_PART);
404 
405  MergingPartition node_equivalence_classes(NumNodes());
406  std::vector<std::vector<int>> permutations_displacing_node(NumNodes());
407  std::vector<int> potential_root_image_nodes;
408  IF_STATS_ENABLED(stats_.initialization_time.StopTimerAndAddElapsedTime());
409 
410  // To find all permutations of the Graph that satisfy the current partition,
411  // we pick an element v that is not in a singleton part, and we
412  // split the search in two phases:
413  // 1) Find (the generators of) all permutations that keep v invariant.
414  // 2) For each w in PartOf(v) such that w != v:
415  // find *one* permutation that maps v to w, if it exists.
416  // if it does exists, add this to the generators.
417  //
418  // The part 1) is recursive.
419  //
420  // Since we can't really use true recursion because it will be too deep for
421  // the stack, we implement it iteratively. To do that, we unroll 1):
422  // the "invariant dive" is a single pass that successively refines the node
423  // base_partition with elements from non-singleton parts (the 'invariant
424  // node'), until all parts are singletons.
425  // We remember which nodes we picked as invariants, and also the successive
426  // partition sizes as we refine it, to allow us to backtrack.
427  // Then we'll perform 2) in the reverse order, backtracking the stack from 1)
428  // as using another dedicated stack for the search (see below).
429  IF_STATS_ENABLED(stats_.invariant_dive_time.StartTimer());
430  struct InvariantDiveState {
431  int invariant_node;
432  int num_parts_before_refinement;
433 
434  InvariantDiveState(int node, int num_parts)
435  : invariant_node(node), num_parts_before_refinement(num_parts) {}
436  };
437  std::vector<InvariantDiveState> invariant_dive_stack;
438  // TODO(user): experiment with, and briefly describe the results of various
439  // algorithms for picking the invariant node:
440  // - random selection
441  // - highest/lowest degree first
442  // - enumerate by part index; or by part size
443  // - etc.
444  for (int invariant_node = 0; invariant_node < NumNodes(); ++invariant_node) {
445  if (base_partition.ElementsInSamePartAs(invariant_node).size() == 1) {
446  continue;
447  }
448  invariant_dive_stack.push_back(
449  InvariantDiveState(invariant_node, base_partition.NumParts()));
450  DistinguishNodeInPartition(invariant_node, &base_partition, nullptr);
451  VLOG(4) << "Invariant dive: invariant node = " << invariant_node
452  << "; partition after: "
453  << base_partition.DebugString(DynamicPartition::SORT_BY_PART);
454  if (time_limit_->LimitReached()) {
455  return absl::Status(absl::StatusCode::kDeadlineExceeded,
456  "During the invariant dive.");
457  }
458  }
459  DenseDoublyLinkedList representatives_sorted_by_index_in_partition(
460  base_partition.ElementsInHierarchicalOrder());
461  DynamicPartition image_partition = base_partition;
462  IF_STATS_ENABLED(stats_.invariant_dive_time.StopTimerAndAddElapsedTime());
463  // Now we've dived to the bottom: we're left with the identity permutation,
464  // which we don't need as a generator. We move on to phase 2).
465 
466  IF_STATS_ENABLED(stats_.main_search_time.StartTimer());
467  while (!invariant_dive_stack.empty()) {
468  if (time_limit_->LimitReached()) break;
469  // Backtrack the last step of 1) (the invariant dive).
470  IF_STATS_ENABLED(stats_.invariant_unroll_time.StartTimer());
471  const int root_node = invariant_dive_stack.back().invariant_node;
472  const int base_num_parts =
473  invariant_dive_stack.back().num_parts_before_refinement;
474  invariant_dive_stack.pop_back();
475  base_partition.UndoRefineUntilNumPartsEqual(base_num_parts);
476  image_partition.UndoRefineUntilNumPartsEqual(base_num_parts);
477  VLOG(4) << "Backtracking invariant dive: root node = " << root_node
478  << "; partition: "
479  << base_partition.DebugString(DynamicPartition::SORT_BY_PART);
480 
481  // Now we'll try to map "root_node" to all image nodes that seem compatible
482  // and that aren't "root_node" itself.
483  //
484  // Doing so, we're able to detect potential bad (or good) matches by
485  // refining the 'base' partition with "root_node"; and refining the
486  // 'image' partition (which represents the partition of images nodes,
487  // i.e. the nodes after applying the currently implicit permutation)
488  // with that candidate image node: if the two partitions don't match, then
489  // the candidate image isn't compatible.
490  // If the partitions do match, we might either find the underlying
491  // permutation directly, or we might need to further try and map other
492  // nodes to their image: this is a recursive search with backtracking.
493 
494  // The potential images of root_node are the nodes in its part. They can be
495  // pruned by the already computed equivalence classes.
496  // TODO(user): better elect the representative of each equivalence class
497  // in order to reduce the permutation support down the line
498  // TODO(user): Don't build a list; but instead use direct, inline iteration
499  // on the representatives in the while() loop below, to benefit from the
500  // incremental merging of the equivalence classes.
501  DCHECK_EQ(1, node_equivalence_classes.NumNodesInSamePartAs(root_node));
502  GetAllOtherRepresentativesInSamePartAs(
503  root_node, base_partition, representatives_sorted_by_index_in_partition,
504  &node_equivalence_classes, &potential_root_image_nodes);
505  DCHECK(!potential_root_image_nodes.empty());
506  IF_STATS_ENABLED(stats_.invariant_unroll_time.StopTimerAndAddElapsedTime());
507 
508  // Try to map "root_node" to all of its potential images. For each image,
509  // we only care about finding a single compatible permutation, if it exists.
510  while (!potential_root_image_nodes.empty()) {
511  if (time_limit_->LimitReached()) break;
512  VLOG(4) << "Potential (pruned) images of root node " << root_node
513  << " left: [" << absl::StrJoin(potential_root_image_nodes, " ")
514  << "].";
515  const int root_image_node = potential_root_image_nodes.back();
516  VLOG(4) << "Trying image of root node: " << root_image_node;
517 
518  std::unique_ptr<SparsePermutation> permutation =
519  FindOneSuitablePermutation(root_node, root_image_node,
520  &base_partition, &image_partition,
521  *generators, permutations_displacing_node);
522 
523  if (permutation != nullptr) {
524  ScopedTimeDistributionUpdater u(&stats_.permutation_output_time);
525  // We found a permutation. We store it in the list of generators, and
526  // further prune out the remaining 'root' image candidates, taking into
527  // account the permutation we just found.
528  MergeNodeEquivalenceClassesAccordingToPermutation(
529  *permutation, &node_equivalence_classes,
530  &representatives_sorted_by_index_in_partition);
531  // HACK(user): to make sure that we keep root_image_node as the
532  // representant of its part, we temporarily move it to the front
533  // of the vector, then move it again to the back so that it gets
534  // deleted by the pop_back() below.
535  SwapFrontAndBack(&potential_root_image_nodes);
536  node_equivalence_classes.KeepOnlyOneNodePerPart(
537  &potential_root_image_nodes);
538  SwapFrontAndBack(&potential_root_image_nodes);
539 
540  // Register it onto the permutations_displacing_node vector.
541  const int permutation_index = static_cast<int>(generators->size());
542  for (const int node : permutation->Support()) {
543  permutations_displacing_node[node].push_back(permutation_index);
544  }
545 
546  // Move the permutation to the generator list (this also transfers
547  // ownership).
548  generators->push_back(std::move(permutation));
549  }
550 
551  potential_root_image_nodes.pop_back();
552  }
553 
554  // We keep track of the size of the orbit of 'root_node' under the
555  // current subgroup: this is one of the factors of the total group size.
556  // TODO(user): better, more complete explanation.
557  factorized_automorphism_group_size->push_back(
558  node_equivalence_classes.NumNodesInSamePartAs(root_node));
559  }
560  node_equivalence_classes.FillEquivalenceClasses(node_equivalence_classes_io);
561  IF_STATS_ENABLED(stats_.main_search_time.StopTimerAndAddElapsedTime());
562  IF_STATS_ENABLED(stats_.SetPrintOrder(StatsGroup::SORT_BY_NAME));
563  IF_STATS_ENABLED(LOG(INFO) << "Statistics: " << stats_.StatString());
564  if (time_limit_->LimitReached()) {
565  return absl::Status(absl::StatusCode::kDeadlineExceeded,
566  "Some automorphisms were found, but probably not all.");
567  }
568  return ::absl::OkStatus();
569 }
570 
571 namespace {
572 // This method can be easily understood in the context of
573 // ConfirmFullMatchOrFindNextMappingDecision(): see its call sites.
574 // Knowing that we want to map some element of part #part_index of
575 // "base_partition" to part #part_index of "image_partition", pick the "best"
576 // such mapping, for the global search algorithm.
577 inline void GetBestMapping(const DynamicPartition& base_partition,
578  const DynamicPartition& image_partition,
579  int part_index, int* base_node, int* image_node) {
580  // As of pending CL 66620435, we've loosely tried three variants of
581  // GetBestMapping():
582  // 1) Just take the first element of the base part, map it to the first
583  // element of the image part.
584  // 2) Just take the first element of the base part, and map it to itself if
585  // possible, else map it to the first element of the image part
586  // 3) Scan all elements of the base parts until we find one that can map to
587  // itself. If there isn't one; we just fall back to the strategy 1).
588  //
589  // Variant 2) gives the best results on most benchmarks, in terms of speed,
590  // but 3) yields much smaller supports for the sat_holeXXX benchmarks, as
591  // long as it's combined with the other tweak enabled by
592  // FLAGS_minimize_permutation_support_size.
593  if (absl::GetFlag(FLAGS_minimize_permutation_support_size)) {
594  // Variant 3).
595  for (const int node : base_partition.ElementsInPart(part_index)) {
596  if (image_partition.PartOf(node) == part_index) {
597  *image_node = *base_node = node;
598  return;
599  }
600  }
601  *base_node = *base_partition.ElementsInPart(part_index).begin();
602  *image_node = *image_partition.ElementsInPart(part_index).begin();
603  return;
604  }
605 
606  // Variant 2).
607  *base_node = *base_partition.ElementsInPart(part_index).begin();
608  if (image_partition.PartOf(*base_node) == part_index) {
609  *image_node = *base_node;
610  } else {
611  *image_node = *image_partition.ElementsInPart(part_index).begin();
612  }
613 }
614 } // namespace
615 
616 // TODO(user): refactor this method and its submethods into a dedicated class
617 // whose members will be ominously accessed by all the class methods; most
618 // notably the search state stack. This may improve readability.
619 std::unique_ptr<SparsePermutation>
620 GraphSymmetryFinder::FindOneSuitablePermutation(
621  int root_node, int root_image_node, DynamicPartition* base_partition,
622  DynamicPartition* image_partition,
623  const std::vector<std::unique_ptr<SparsePermutation>>&
624  generators_found_so_far,
625  const std::vector<std::vector<int>>& permutations_displacing_node) {
626  // DCHECKs() and statistics.
627  ScopedTimeDistributionUpdater search_time_updater(&stats_.search_time);
628  DCHECK_EQ("", tmp_dynamic_permutation_.DebugString());
629  DCHECK_EQ(base_partition->DebugString(DynamicPartition::SORT_BY_PART),
630  image_partition->DebugString(DynamicPartition::SORT_BY_PART));
631  DCHECK(search_states_.empty());
632 
633  // These will be used during the search. See their usage.
634  std::vector<int> base_singletons;
635  std::vector<int> image_singletons;
636  int next_base_node;
637  int next_image_node;
638  int min_potential_mismatching_part_index;
639  std::vector<int> next_potential_image_nodes;
640 
641  // Initialize the search: we can already distinguish "root_node" in the base
642  // partition. See the comment below.
643  search_states_.emplace_back(
644  /*base_node=*/root_node, /*first_image_node=*/-1,
645  /*num_parts_before_trying_to_map_base_node=*/base_partition->NumParts(),
646  /*min_potential_mismatching_part_index=*/base_partition->NumParts());
647  // We inject the image node directly as the "remaining_pruned_image_nodes".
648  search_states_.back().remaining_pruned_image_nodes.assign(1, root_image_node);
649  {
650  ScopedTimeDistributionUpdater u(&stats_.initial_search_refine_time);
651  DistinguishNodeInPartition(root_node, base_partition, &base_singletons);
652  }
653  while (!search_states_.empty()) {
654  if (time_limit_->LimitReached()) return nullptr;
655  // When exploring a SearchState "ss", we're supposed to have:
656  // - A base_partition that has already been refined on ss->base_node.
657  // (base_singleton is the list of singletons created on the base
658  // partition during that refinement).
659  // - A non-empty list of potential image nodes (we'll try them in reverse
660  // order).
661  // - An image partition that hasn't been refined yet.
662  //
663  // Also, one should note that the base partition (before its refinement on
664  // base_node) was deemed compatible with the image partition as it is now.
665  const SearchState& ss = search_states_.back();
666  const int image_node = ss.first_image_node >= 0
667  ? ss.first_image_node
668  : ss.remaining_pruned_image_nodes.back();
669 
670  // Statistics, DCHECKs.
671  IF_STATS_ENABLED(stats_.search_depth.Add(search_states_.size()));
672  DCHECK_EQ(ss.num_parts_before_trying_to_map_base_node,
673  image_partition->NumParts());
674 
675  // Apply the decision: map base_node to image_node. Since base_partition
676  // was already refined on base_node, we just need to refine image_partition.
677  {
678  ScopedTimeDistributionUpdater u(&stats_.search_refine_time);
679  DistinguishNodeInPartition(image_node, image_partition,
680  &image_singletons);
681  }
682  VLOG(4) << ss.DebugString();
683  VLOG(4) << base_partition->DebugString(DynamicPartition::SORT_BY_PART);
684  VLOG(4) << image_partition->DebugString(DynamicPartition::SORT_BY_PART);
685 
686  // Run some diagnoses on the two partitions. There are many outcomes, so
687  // it's a bit complicated:
688  // 1) The partitions are incompatible
689  // - Because of a straightfoward criterion (size mismatch).
690  // - Because they are both fully refined (i.e. singletons only), yet the
691  // permutation induced by them is not a graph automorpshim.
692  // 2) The partitions induce a permutation (all their non-singleton parts are
693  // identical), and this permutation is a graph automorphism.
694  // 3) The partitions need further refinement:
695  // - Because some non-singleton parts aren't equal in the base and image
696  // partition
697  // - Or because they are a full match (i.e. may induce a permutation,
698  // like in 2)), but the induced permutation isn't a graph automorphism.
699  bool compatible = true;
700  {
701  ScopedTimeDistributionUpdater u(&stats_.quick_compatibility_time);
702  compatible = PartitionsAreCompatibleAfterPartIndex(
703  *base_partition, *image_partition,
704  ss.num_parts_before_trying_to_map_base_node);
705  u.AlsoUpdate(compatible ? &stats_.quick_compatibility_success_time
706  : &stats_.quick_compatibility_fail_time);
707  }
708  bool partitions_are_full_match = false;
709  if (compatible) {
710  {
712  &stats_.dynamic_permutation_refinement_time);
713  tmp_dynamic_permutation_.AddMappings(base_singletons, image_singletons);
714  }
715  ScopedTimeDistributionUpdater u(&stats_.map_election_std_time);
716  min_potential_mismatching_part_index =
717  ss.min_potential_mismatching_part_index;
718  partitions_are_full_match = ConfirmFullMatchOrFindNextMappingDecision(
719  *base_partition, *image_partition, tmp_dynamic_permutation_,
720  &min_potential_mismatching_part_index, &next_base_node,
721  &next_image_node);
722  u.AlsoUpdate(partitions_are_full_match
723  ? &stats_.map_election_std_full_match_time
724  : &stats_.map_election_std_mapping_time);
725  }
726  if (compatible && partitions_are_full_match) {
727  DCHECK_EQ(min_potential_mismatching_part_index,
728  base_partition->NumParts());
729  // We have a permutation candidate!
730  // Note(user): we also deal with (extremely rare) false positives for
731  // "partitions_are_full_match" here: in case they aren't a full match,
732  // IsGraphAutomorphism() will catch that; and we'll simply deepen the
733  // search.
734  bool is_automorphism = true;
735  {
736  ScopedTimeDistributionUpdater u(&stats_.automorphism_test_time);
737  is_automorphism = IsGraphAutomorphism(tmp_dynamic_permutation_);
738  u.AlsoUpdate(is_automorphism ? &stats_.automorphism_test_success_time
739  : &stats_.automorphism_test_fail_time);
740  }
741  if (is_automorphism) {
742  ScopedTimeDistributionUpdater u(&stats_.search_finalize_time);
743  // We found a valid permutation. We can return it, but first we
744  // must restore the partitions to their original state.
745  std::unique_ptr<SparsePermutation> sparse_permutation(
746  tmp_dynamic_permutation_.CreateSparsePermutation());
747  VLOG(4) << "Automorphism found: " << sparse_permutation->DebugString();
748  const int base_num_parts =
749  search_states_[0].num_parts_before_trying_to_map_base_node;
750  base_partition->UndoRefineUntilNumPartsEqual(base_num_parts);
751  image_partition->UndoRefineUntilNumPartsEqual(base_num_parts);
752  tmp_dynamic_permutation_.Reset();
753  search_states_.clear();
754 
755  search_time_updater.AlsoUpdate(&stats_.search_time_success);
756  return sparse_permutation;
757  }
758 
759  // The permutation isn't a valid automorphism. Either the partitions were
760  // fully refined, and we deem them incompatible, or they weren't, and we
761  // consider them as 'not a full match'.
762  VLOG(4) << "Permutation candidate isn't a valid automorphism.";
763  if (base_partition->NumParts() == NumNodes()) {
764  // Fully refined: the partitions are incompatible.
765  compatible = false;
766  ScopedTimeDistributionUpdater u(&stats_.dynamic_permutation_undo_time);
767  tmp_dynamic_permutation_.UndoLastMappings(&base_singletons);
768  } else {
769  ScopedTimeDistributionUpdater u(&stats_.map_reelection_time);
770  // TODO(user, viger): try to get the non-singleton part from
771  // DynamicPermutation in O(1). On some graphs like the symmetry of the
772  // mip problem lectsched-4-obj.mps.gz, this take the majority of the
773  // time!
774  int non_singleton_part = 0;
775  {
776  ScopedTimeDistributionUpdater u(&stats_.non_singleton_search_time);
777  while (base_partition->SizeOfPart(non_singleton_part) == 1) {
778  ++non_singleton_part;
779  DCHECK_LT(non_singleton_part, base_partition->NumParts());
780  }
781  }
782  time_limit_->AdvanceDeterministicTime(
783  1e-9 * static_cast<double>(non_singleton_part));
784 
785  // The partitions are compatible, but we'll deepen the search on some
786  // non-singleton part. We can pick any base and image node in this case.
787  GetBestMapping(*base_partition, *image_partition, non_singleton_part,
788  &next_base_node, &next_image_node);
789  }
790  }
791 
792  // Now we've fully diagnosed our partitions, and have already dealt with
793  // case 2). We're left to deal with 1) and 3).
794  //
795  // Case 1): partitions are incompatible.
796  if (!compatible) {
797  ScopedTimeDistributionUpdater u(&stats_.backtracking_time);
798  // We invalidate the current image node, and prune the remaining image
799  // nodes. We might be left with no other image nodes, which means that
800  // we'll backtrack, i.e. pop our current SearchState and invalidate the
801  // 'current' image node of the upper SearchState (which might lead to us
802  // backtracking it, and so on).
803  while (!search_states_.empty()) {
804  SearchState* const last_ss = &search_states_.back();
805  image_partition->UndoRefineUntilNumPartsEqual(
806  last_ss->num_parts_before_trying_to_map_base_node);
807  if (last_ss->first_image_node >= 0) {
808  // Find out and prune the remaining potential image nodes: there is
809  // no permutation that maps base_node -> image_node that is
810  // compatible with the current partition, so there can't be a
811  // permutation that maps base_node -> X either, for all X in the orbit
812  // of 'image_node' under valid permutations compatible with the
813  // current partition. Ditto for other potential image nodes.
814  //
815  // TODO(user): fix this: we should really be collecting all
816  // permutations displacing any node in "image_part", for the pruning
817  // to be really exhaustive. We could also consider alternative ways,
818  // like incrementally maintaining the list of permutations compatible
819  // with the partition so far.
820  const int part = image_partition->PartOf(last_ss->first_image_node);
821  last_ss->remaining_pruned_image_nodes.reserve(
822  image_partition->SizeOfPart(part));
823  last_ss->remaining_pruned_image_nodes.push_back(
824  last_ss->first_image_node);
825  for (const int e : image_partition->ElementsInPart(part)) {
826  if (e != last_ss->first_image_node) {
827  last_ss->remaining_pruned_image_nodes.push_back(e);
828  }
829  }
830  {
831  ScopedTimeDistributionUpdater u(&stats_.pruning_time);
832  PruneOrbitsUnderPermutationsCompatibleWithPartition(
833  *image_partition, generators_found_so_far,
834  permutations_displacing_node[last_ss->first_image_node],
835  &last_ss->remaining_pruned_image_nodes);
836  }
837  SwapFrontAndBack(&last_ss->remaining_pruned_image_nodes);
838  DCHECK_EQ(last_ss->remaining_pruned_image_nodes.back(),
839  last_ss->first_image_node);
840  last_ss->first_image_node = -1;
841  }
842  last_ss->remaining_pruned_image_nodes.pop_back();
843  if (!last_ss->remaining_pruned_image_nodes.empty()) break;
844 
845  VLOG(4) << "Backtracking one level up.";
846  base_partition->UndoRefineUntilNumPartsEqual(
847  last_ss->num_parts_before_trying_to_map_base_node);
848  // If this was the root search state (i.e. we fully backtracked and
849  // will exit the search after that), we don't have mappings to undo.
850  // We run UndoLastMappings() anyway, because it's a no-op in that case.
851  tmp_dynamic_permutation_.UndoLastMappings(&base_singletons);
852  search_states_.pop_back();
853  }
854  // Continue the search.
855  continue;
856  }
857 
858  // Case 3): we deepen the search.
859  // Since the search loop starts from an already-refined base_partition,
860  // we must do it here.
861  VLOG(4) << " Deepening the search.";
862  search_states_.emplace_back(
863  next_base_node, next_image_node,
864  /*num_parts_before_trying_to_map_base_node*/ base_partition->NumParts(),
865  min_potential_mismatching_part_index);
866  {
867  ScopedTimeDistributionUpdater u(&stats_.search_refine_time);
868  DistinguishNodeInPartition(next_base_node, base_partition,
869  &base_singletons);
870  }
871  }
872  // We exhausted the search; we didn't find any permutation.
873  search_time_updater.AlsoUpdate(&stats_.search_time_fail);
874  return nullptr;
875 }
876 
878 GraphSymmetryFinder::TailsOfIncomingArcsTo(int node) const {
880  flattened_reverse_adj_lists_.begin() + reverse_adj_list_index_[node],
881  flattened_reverse_adj_lists_.begin() + reverse_adj_list_index_[node + 1]);
882 }
883 
884 void GraphSymmetryFinder::PruneOrbitsUnderPermutationsCompatibleWithPartition(
885  const DynamicPartition& partition,
886  const std::vector<std::unique_ptr<SparsePermutation>>& permutations,
887  const std::vector<int>& permutation_indices, std::vector<int>* nodes) {
888  VLOG(4) << " Pruning [" << absl::StrJoin(*nodes, ", ") << "]";
889  // TODO(user): apply a smarter test to decide whether to do the pruning
890  // or not: we can accurately estimate the cost of pruning (iterate through
891  // all generators found so far) and its estimated benefit (the cost of
892  // the search below the state that we're currently in, times the expected
893  // number of pruned nodes). Sometimes it may be better to skip the
894  // pruning.
895  if (nodes->size() <= 1) return;
896 
897  // Iterate on all targeted permutations. If they are compatible, apply
898  // them to tmp_partition_ which will contain the incrementally merged
899  // equivalence classes.
900  std::vector<int>& tmp_nodes_on_support =
901  tmp_stack_; // Rename, for readability.
902  DCHECK(tmp_nodes_on_support.empty());
903  // TODO(user): investigate further optimizations: maybe it's possible
904  // to incrementally maintain the set of permutations that is compatible
905  // with the current partition, instead of recomputing it here?
906  for (const int p : permutation_indices) {
907  const SparsePermutation& permutation = *permutations[p];
908  // First, a quick compatibility check: the permutation's cycles must be
909  // smaller or equal to the size of the part that they are included in.
910  bool compatible = true;
911  for (int c = 0; c < permutation.NumCycles(); ++c) {
912  const SparsePermutation::Iterator cycle = permutation.Cycle(c);
913  if (cycle.size() >
914  partition.SizeOfPart(partition.PartOf(*cycle.begin()))) {
915  compatible = false;
916  break;
917  }
918  }
919  if (!compatible) continue;
920  // Now the full compatibility check: each cycle of the permutation must
921  // be fully included in an image part.
922  for (int c = 0; c < permutation.NumCycles(); ++c) {
923  int part = -1;
924  for (const int node : permutation.Cycle(c)) {
925  if (partition.PartOf(node) != part) {
926  if (part >= 0) {
927  compatible = false;
928  break;
929  }
930  part = partition.PartOf(node); // Initilization of 'part'.
931  }
932  }
933  }
934  if (!compatible) continue;
935  // The permutation is fully compatible!
936  // TODO(user): ignore cycles that are outside of image_part.
937  MergeNodeEquivalenceClassesAccordingToPermutation(permutation,
938  &tmp_partition_, nullptr);
939  for (const int node : permutation.Support()) {
940  if (!tmp_node_mask_[node]) {
941  tmp_node_mask_[node] = true;
942  tmp_nodes_on_support.push_back(node);
943  }
944  }
945  }
946 
947  // Apply the pruning.
948  tmp_partition_.KeepOnlyOneNodePerPart(nodes);
949 
950  // Reset the "tmp_" structures sparsely.
951  for (const int node : tmp_nodes_on_support) {
952  tmp_node_mask_[node] = false;
953  tmp_partition_.ResetNode(node);
954  }
955  tmp_nodes_on_support.clear();
956  VLOG(4) << " Pruned: [" << absl::StrJoin(*nodes, ", ") << "]";
957 }
958 
959 bool GraphSymmetryFinder::ConfirmFullMatchOrFindNextMappingDecision(
960  const DynamicPartition& base_partition,
961  const DynamicPartition& image_partition,
962  const DynamicPermutation& current_permutation_candidate,
963  int* min_potential_mismatching_part_index_io, int* next_base_node,
964  int* next_image_node) const {
965  *next_base_node = -1;
966  *next_image_node = -1;
967 
968  // The following clause should be true most of the times, except in some
969  // specific use cases.
970  if (!absl::GetFlag(FLAGS_minimize_permutation_support_size)) {
971  // First, we try to map the loose ends of the current permutations: these
972  // loose ends can't be mapped to themselves, so we'll have to map them to
973  // something anyway.
974  for (const int loose_node : current_permutation_candidate.LooseEnds()) {
975  DCHECK_GT(base_partition.ElementsInSamePartAs(loose_node).size(), 1);
976  *next_base_node = loose_node;
977  const int root = current_permutation_candidate.RootOf(loose_node);
978  DCHECK_NE(root, loose_node);
979  if (image_partition.PartOf(root) == base_partition.PartOf(loose_node)) {
980  // We prioritize mapping a loose end to its own root (i.e. close a
981  // cycle), if possible, like here: we exit immediately.
982  *next_image_node = root;
983  return false;
984  }
985  }
986  if (*next_base_node != -1) {
987  // We found loose ends, but none that mapped to its own root. Just pick
988  // any valid image.
989  *next_image_node =
990  *image_partition
991  .ElementsInPart(base_partition.PartOf(*next_base_node))
992  .begin();
993  return false;
994  }
995  }
996 
997  // If there is no loose node (i.e. the current permutation only has closed
998  // cycles), we fall back to picking any part that is different in the base and
999  // image partitions; because we know that some mapping decision will have to
1000  // be made there.
1001  // SUBTLE: we use "min_potential_mismatching_part_index_io" to incrementally
1002  // keep running this search (for a mismatching part) from where we left off.
1003  // TODO(user): implement a simpler search for a mismatching part: it's
1004  // trivially possible if the base partition maintains a hash set of all
1005  // Fprints of its parts, and if the image partition uses that to maintain the
1006  // list of 'different' non-singleton parts.
1007  const int initial_min_potential_mismatching_part_index =
1008  *min_potential_mismatching_part_index_io;
1009  for (; *min_potential_mismatching_part_index_io < base_partition.NumParts();
1010  ++*min_potential_mismatching_part_index_io) {
1011  const int p = *min_potential_mismatching_part_index_io;
1012  if (base_partition.SizeOfPart(p) != 1 &&
1013  base_partition.FprintOfPart(p) != image_partition.FprintOfPart(p)) {
1014  GetBestMapping(base_partition, image_partition, p, next_base_node,
1015  next_image_node);
1016  return false;
1017  }
1018 
1019  const int parent = base_partition.ParentOfPart(p);
1020  if (parent < initial_min_potential_mismatching_part_index &&
1021  base_partition.SizeOfPart(parent) != 1 &&
1022  base_partition.FprintOfPart(parent) !=
1023  image_partition.FprintOfPart(parent)) {
1024  GetBestMapping(base_partition, image_partition, parent, next_base_node,
1025  next_image_node);
1026  return false;
1027  }
1028  }
1029 
1030  // We didn't find an unequal part. DCHECK that our "incremental" check was
1031  // actually correct and that all non-singleton parts are indeed equal.
1032  if (DEBUG_MODE) {
1033  for (int p = 0; p < base_partition.NumParts(); ++p) {
1034  if (base_partition.SizeOfPart(p) != 1) {
1035  CHECK_EQ(base_partition.FprintOfPart(p),
1036  image_partition.FprintOfPart(p));
1037  }
1038  }
1039  }
1040  return true;
1041 }
1042 
1043 std::string GraphSymmetryFinder::SearchState::DebugString() const {
1044  return absl::StrFormat(
1045  "SearchState{ base_node=%d, first_image_node=%d,"
1046  " remaining_pruned_image_nodes=[%s],"
1047  " num_parts_before_trying_to_map_base_node=%d }",
1048  base_node, first_image_node,
1049  absl::StrJoin(remaining_pruned_image_nodes, " "),
1050  num_parts_before_trying_to_map_base_node);
1051 }
1052 
1053 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
#define DCHECK_NE(val1, val2)
Definition: base/logging.h:894
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:705
#define DCHECK_GT(val1, val2)
Definition: base/logging.h:898
#define DCHECK_LT(val1, val2)
Definition: base/logging.h:896
#define LOG(severity)
Definition: base/logging.h:423
#define DCHECK(condition)
Definition: base/logging.h:892
#define DCHECK_EQ(val1, val2)
Definition: base/logging.h:893
#define VLOG(verboselevel)
Definition: base/logging.h:986
IterablePart ElementsInPart(int i) const
void Refine(const std::vector< int > &distinguished_subset)
const std::vector< int > & ElementsInHierarchicalOrder() const
void UndoRefineUntilNumPartsEqual(int original_num_parts)
IterablePart ElementsInSamePartAs(int i) const
std::string DebugString(DebugStringSorting sorting) const
std::unique_ptr< SparsePermutation > CreateSparsePermutation() const
const std::vector< int > & AllMappingsSrc() const
void UndoLastMappings(std::vector< int > *undone_mapping_src)
void AddMappings(const std::vector< int > &src, const std::vector< int > &dst)
void RecursivelyRefinePartitionByAdjacency(int first_unrefined_part_index, DynamicPartition *partition)
bool IsGraphAutomorphism(const DynamicPermutation &permutation) const
void DistinguishNodeInPartition(int node, DynamicPartition *partition, std::vector< int > *new_singletons_or_null)
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)
GraphSymmetryFinder(const Graph &graph, bool is_undirected)
int MergePartsOf(int node1, int node2)
int FillEquivalenceClasses(std::vector< int > *node_equivalence_classes)
void KeepOnlyOneNodePerPart(std::vector< int > *nodes)
A simple class to enforce both an elapsed time limit and a deterministic time limit in the same threa...
Definition: time_limit.h:105
bool LimitReached()
Returns true when the external limit is true, or the deterministic time is over the deterministic lim...
Definition: time_limit.h:532
void AdvanceDeterministicTime(double deterministic_duration)
Advances the deterministic time.
Definition: time_limit.h:226
ArcIndexType num_arcs() const
Definition: graph.h:206
NodeIndexType num_nodes() const
Definition: graph.h:203
IntegerRange< NodeIndex > AllNodes() const
Definition: graph.h:936
NodeIndexType Head(ArcIndexType arc) const
Definition: graph.h:1314
BeginEndWrapper< OutgoingArcIterator > OutgoingArcs(NodeIndexType node) const
SharedTimeLimit * time_limit
ABSL_FLAG(bool, minimize_permutation_support_size, false, "Tweak the algorithm to try and minimize the support size" " of the generators produced. This may negatively impact the" " performance, but works great on the sat_holeXXX benchmarks" " to reduce the support size.")
const int INFO
Definition: log_severity.h:31
const bool DEBUG_MODE
Definition: macros.h:24
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:263
Collection of objects used to extend the Constraint Solver library.
DisabledScopedTimeDistributionUpdater ScopedTimeDistributionUpdater
Definition: stats.h:434
bool GraphIsSymmetric(const Graph &graph)
Definition: graph/util.h:218
int nodes
#define IF_STATS_ENABLED(instructions)
Definition: stats.h:437
std::vector< int >::const_iterator begin() const