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