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