Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
symmetry_util.cc
Go to the documentation of this file.
1// Copyright 2010-2025 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 <memory>
19#include <vector>
20
21#include "absl/container/flat_hash_set.h"
22#include "absl/log/check.h"
23#include "absl/types/span.h"
27
28namespace operations_research {
29namespace sat {
30
31std::vector<std::vector<int>> BasicOrbitopeExtraction(
32 absl::Span<const std::unique_ptr<SparsePermutation>> generators) {
33 // Count the number of permutations that are compositions of 2-cycle and
34 // regroup them according to the number of cycles.
35 std::vector<std::vector<int>> num_cycles_to_2cyclers;
36 for (int g = 0; g < generators.size(); ++g) {
37 const std::unique_ptr<SparsePermutation>& perm = generators[g];
38 bool contain_only_2cycles = true;
39 const int num_cycles = perm->NumCycles();
40 for (int i = 0; i < num_cycles; ++i) {
41 if (perm->Cycle(i).size() != 2) {
42 contain_only_2cycles = false;
43 break;
44 }
45 }
46 if (!contain_only_2cycles) continue;
47 if (num_cycles >= num_cycles_to_2cyclers.size()) {
48 num_cycles_to_2cyclers.resize(num_cycles + 1);
49 }
50 num_cycles_to_2cyclers[num_cycles].push_back(g);
51 }
52
53 // Heuristic: we try to grow the orbitope that has the most potential for
54 // fixing variables.
55 //
56 // TODO(user): We could grow each and keep the real maximum.
57 int best = -1;
58 int best_score = 0;
59 for (int i = 0; i < num_cycles_to_2cyclers.size(); ++i) {
60 if (num_cycles_to_2cyclers[i].size() > 1) {
61 const int num_perms = num_cycles_to_2cyclers[i].size() + 1;
62 VLOG(1) << "Potential orbitope: " << i << " x " << num_perms;
63 const int64_t score = std::min(i, num_perms);
64 if (score > best_score) {
65 best = i;
66 best_score = score;
67 }
68 }
69 }
70
71 std::vector<std::vector<int>> orbitope;
72 if (best == -1) return orbitope;
73
74 // We will track the element already added so we never have duplicates.
75 std::vector<bool> in_matrix;
76
77 // Greedily grow the orbitope.
78 orbitope.resize(best);
79 for (const int g : num_cycles_to_2cyclers[best]) {
80 // Start using the first permutation.
81 if (orbitope[0].empty()) {
82 const std::unique_ptr<SparsePermutation>& perm = generators[g];
83 const int num_cycles = perm->NumCycles();
84 for (int i = 0; i < num_cycles; ++i) {
85 for (const int x : perm->Cycle(i)) {
86 orbitope[i].push_back(x);
87 if (x >= in_matrix.size()) in_matrix.resize(x + 1, false);
88 in_matrix[x] = true;
89 }
90 }
91 continue;
92 }
93
94 // We want to find a column such that g sends it to variables not already
95 // in the orbitope matrix.
96 //
97 // Note(user): This relies on the cycle in each permutation to be ordered by
98 // smaller element first. This way we don't have to account any row
99 // permutation of the orbitope matrix. The code that detect the symmetries
100 // of the problem should already return permutation in this canonical
101 // format.
102 std::vector<int> grow;
103 int matching_column_index = -1;
104 const std::unique_ptr<SparsePermutation>& perm = generators[g];
105 const int num_cycles = perm->NumCycles();
106 for (int i = 0; i < num_cycles; ++i) {
107 // Extract the two elements of this transposition.
108 std::vector<int> tmp;
109 for (const int x : perm->Cycle(i)) tmp.push_back(x);
110 const int a = tmp[0];
111 const int b = tmp[1];
112
113 // We want one element to appear in matching_column_index and the other to
114 // not appear at all.
115 int num_matches_a = 0;
116 int num_matches_b = 0;
117 int last_match_index = -1;
118 for (int j = 0; j < orbitope[i].size(); ++j) {
119 if (orbitope[i][j] == a) {
120 ++num_matches_a;
121 last_match_index = j;
122 } else if (orbitope[i][j] == b) {
123 ++num_matches_b;
124 last_match_index = j;
125 }
126 }
127 if (last_match_index == -1) break;
128 if (matching_column_index == -1) {
129 matching_column_index = last_match_index;
130 }
131 if (matching_column_index != last_match_index) break;
132 if (num_matches_a == 0 && num_matches_b == 1) {
133 if (a >= in_matrix.size() || !in_matrix[a]) grow.push_back(a);
134 } else if (num_matches_a == 1 && num_matches_b == 0) {
135 if (b >= in_matrix.size() || !in_matrix[b]) grow.push_back(b);
136 } else {
137 break;
138 }
139 }
140
141 // If grow is of full size, we can extend the orbitope.
142 if (grow.size() == num_cycles) {
143 for (int i = 0; i < orbitope.size(); ++i) {
144 orbitope[i].push_back(grow[i]);
145 if (grow[i] >= in_matrix.size()) in_matrix.resize(grow[i] + 1, false);
146 in_matrix[grow[i]] = true;
147 }
148 }
149 }
150
151 return orbitope;
152}
153
154std::vector<int> GetOrbits(
155 int n, absl::Span<const std::unique_ptr<SparsePermutation>> generators) {
156 MergingPartition union_find;
157 union_find.Reset(n);
158 for (const std::unique_ptr<SparsePermutation>& perm : generators) {
159 DCHECK(perm != nullptr);
160 const int num_cycles = perm->NumCycles();
161 for (int i = 0; i < num_cycles; ++i) {
162 // Note that there is currently no random access api like cycle[j].
163 int first;
164 bool is_first = true;
165 for (const int x : perm->Cycle(i)) {
166 DCHECK_GE(x, 0);
167 DCHECK_LT(x, n);
168 if (is_first) {
169 first = x;
170 is_first = false;
171 } else {
172 union_find.MergePartsOf(first, x);
173 }
174 }
175 }
176 }
177
178 int num_parts = 0;
179 std::vector<int> orbits(n, -1);
180 for (int i = 0; i < n; ++i) {
181 if (union_find.NumNodesInSamePartAs(i) == 1) continue;
182 const int root = union_find.GetRootAndCompressPath(i);
183 if (orbits[root] == -1) orbits[root] = num_parts++;
184 orbits[i] = orbits[root];
185 }
186 return orbits;
187}
188
189std::vector<int> GetOrbitopeOrbits(
190 int n, absl::Span<const std::vector<int>> orbitope) {
191 std::vector<int> orbits(n, -1);
192 for (int i = 0; i < orbitope.size(); ++i) {
193 for (int j = 0; j < orbitope[i].size(); ++j) {
194 CHECK_EQ(orbits[orbitope[i][j]], -1);
195 orbits[orbitope[i][j]] = i;
196 }
197 }
198 return orbits;
199}
200
202 int point, absl::Span<const std::unique_ptr<SparsePermutation>> generators,
203 std::vector<int>* schrier_vector, std::vector<int>* orbit) {
204 schrier_vector->clear();
205 *orbit = {point};
206 if (generators.empty()) return;
207 schrier_vector->resize(generators[0]->Size(), -1);
208 absl::flat_hash_set<int> orbit_set = {point};
209 for (int i = 0; i < orbit->size(); ++i) {
210 const int orbit_element = (*orbit)[i];
211 for (int i = 0; i < generators.size(); ++i) {
212 DCHECK_EQ(schrier_vector->size(), generators[i]->Size());
213 const int image = generators[i]->Image(orbit_element);
214 if (image == orbit_element) continue;
215 const auto [it, inserted] = orbit_set.insert(image);
216 if (inserted) {
217 (*schrier_vector)[image] = i;
218 orbit->push_back(image);
219 }
220 }
221 }
222}
223
224std::vector<int> TracePoint(
225 int point, absl::Span<const int> schrier_vector,
226 absl::Span<const std::unique_ptr<SparsePermutation>> generators) {
227 std::vector<int> result;
228 while (schrier_vector[point] != -1) {
229 const SparsePermutation& perm = *generators[schrier_vector[point]];
230 result.push_back(schrier_vector[point]);
231 const int next = perm.InverseImage(point);
232 DCHECK_NE(next, point);
233 point = next;
234 }
235 return result;
236}
237
238std::unique_ptr<SparsePermutation> CreateSparsePermutationFromProto(
239 int n, const SparsePermutationProto& proto) {
240 auto perm = std::make_unique<SparsePermutation>(n);
241 int support_index = 0;
242 const int num_cycle = proto.cycle_sizes().size();
243 for (int i = 0; i < num_cycle; ++i) {
244 const int size = proto.cycle_sizes(i);
245 for (int j = 0; j < size; ++j) {
246 DCHECK_LT(proto.support(support_index), n);
247 perm->AddToCurrentCycle(proto.support(support_index++));
248 }
249 perm->CloseCurrentCycle();
250 }
251 return perm;
252}
253
254} // namespace sat
255} // namespace operations_research
std::vector< int > GetOrbitopeOrbits(int n, absl::Span< const std::vector< int > > orbitope)
void GetSchreierVectorAndOrbit(int point, absl::Span< const std::unique_ptr< SparsePermutation > > generators, std::vector< int > *schrier_vector, std::vector< int > *orbit)
std::vector< int > TracePoint(int point, absl::Span< const int > schrier_vector, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
std::vector< int > GetOrbits(int n, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
std::unique_ptr< SparsePermutation > CreateSparsePermutationFromProto(int n, const SparsePermutationProto &proto)
Creates a SparsePermutation on [0, n) from its proto representation.
std::vector< std::vector< int > > BasicOrbitopeExtraction(absl::Span< const std::unique_ptr< SparsePermutation > > generators)
In SWIG mode, we don't want anything besides these top-level includes.