Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cliques.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 <functional>
18#include <memory>
19#include <utility>
20#include <vector>
21
22#include "absl/container/flat_hash_set.h"
23#include "absl/log/check.h"
24#include "ortools/util/bitset.h"
25
26namespace operations_research {
27namespace {
28// Encapsulates graph() to make all nodes self-connected.
29inline bool Connects(std::function<bool(int, int)> graph, int i, int j) {
30 return i == j || graph(i, j);
31}
32
33// Implements the recursive step of the Bron-Kerbosch algorithm with pivoting.
34// - graph is a callback such that graph->Run(i, j) returns true iff there is an
35// arc between i and j.
36// - callback is a callback called for all maximal cliques discovered by the
37// algorithm.
38// - input_candidates is an array that contains the list of nodes connected to
39// all nodes in the current clique. It is composed of two parts; the first
40// part contains the "not" set (nodes that were already processed and must not
41// be added to the clique - see the description of the algorithm in the
42// paper), and nodes that are candidates for addition. The candidates from the
43// "not" set are at the beginning of the array.
44// - first_candidate_index elements is the index of the first candidate that is
45// not in the "not" set (which is also the number of candidates in the "not"
46// set).
47// - num_input_candidates is the number of elements in input_candidates,
48// including both the "not" set and the actual candidates.
49// - current_clique is the current clique discovered by the algorithm.
50// - stop is a stopping condition for the algorithm; if the value it points to
51// is true, the algorithm stops further exploration and returns.
52// TODO(user) : rewrite this algorithm without recursion.
53void Search(std::function<bool(int, int)> graph,
54 std::function<bool(const std::vector<int>&)> callback,
55 int* input_candidates, int first_candidate_index,
56 int num_input_candidates, std::vector<int>* current_clique,
57 bool* stop) {
58 // The pivot is a node from input_candidates that is disconnected from the
59 // minimal number of nodes in the actual candidates (excluding the "not" set);
60 // the algorithm then selects only candidates that are disconnected from the
61 // pivot (and the pivot itself), to reach the termination condition as quickly
62 // as possible (see the original paper for more details).
63 int pivot = 0;
64
65 // A node that is disconnected from the selected pivot. This node is selected
66 // during the pivot matching phase to speed up the first iteration of the
67 // recursive call.
68 int disconnected_node = 0;
69
70 // The number of candidates (that are not in "not") disconnected from the
71 // selected pivot. The value is computed during pivot selection. In the
72 // "recursive" phase, we only need to do explore num_disconnected_candidates
73 // nodes, because after this step, all remaining candidates will all be
74 // connected to the pivot node (which is in "not"), so they can't form a
75 // maximal clique.
76 int num_disconnected_candidates = num_input_candidates;
77
78 // If the selected pivot is not in "not", we need to process one more
79 // candidate (the pivot itself). pre_increment is added to
80 // num_disconnected_candidates to compensate for this fact.
81 int pre_increment = 0;
82
83 // Find Pivot.
84 for (int i = 0; i < num_input_candidates && num_disconnected_candidates != 0;
85 ++i) {
86 int pivot_candidate = input_candidates[i];
87
88 // Count is the number of candidates (not including nodes in the "not" set)
89 // that are disconnected from the pivot candidate.
90 int count = 0;
91
92 // The index of a candidate node that is not connected to pivot_candidate.
93 // This node will be used to quickly start the nested iteration (we keep
94 // track of the index so that we don't have to find a node that is
95 // disconnected from the pivot later in the iteration).
96 int disconnected_node_candidate = 0;
97
98 // Compute the number of candidate nodes that are disconnected from
99 // pivot_candidate. Note that this computation is the same for the "not"
100 // candidates and the normal candidates.
101 for (int j = first_candidate_index;
102 j < num_input_candidates && count < num_disconnected_candidates; ++j) {
103 if (!Connects(graph, pivot_candidate, input_candidates[j])) {
104 count++;
105 disconnected_node_candidate = j;
106 }
107 }
108
109 // Update the pivot candidate if we found a new minimum for
110 // num_disconnected_candidates.
111 if (count < num_disconnected_candidates) {
112 pivot = pivot_candidate;
113 num_disconnected_candidates = count;
114
115 if (i < first_candidate_index) {
116 disconnected_node = disconnected_node_candidate;
117 } else {
118 disconnected_node = i;
119 // The pivot candidate is not in the "not" set. We need to pre-increment
120 // the counter for the node to compensate for that.
121 pre_increment = 1;
122 }
123 }
124 }
125
126 std::vector<int> new_candidates;
127 new_candidates.reserve(num_input_candidates);
128 for (int remaining_candidates = num_disconnected_candidates + pre_increment;
129 remaining_candidates >= 1; remaining_candidates--) {
130 // Swap a node that is disconnected from the pivot (or the pivot itself)
131 // with the first candidate, so that we can later move it to "not" simply by
132 // increasing the index of the first candidate that is not in "not".
133 const int selected = input_candidates[disconnected_node];
134 std::swap(input_candidates[disconnected_node],
135 input_candidates[first_candidate_index]);
136
137 // Fill the list of candidates and the "not" set for the recursive call:
138 new_candidates.clear();
139 for (int i = 0; i < first_candidate_index; ++i) {
140 if (Connects(graph, selected, input_candidates[i])) {
141 new_candidates.push_back(input_candidates[i]);
142 }
143 }
144 const int new_first_candidate_index = new_candidates.size();
145 for (int i = first_candidate_index + 1; i < num_input_candidates; ++i) {
146 if (Connects(graph, selected, input_candidates[i])) {
147 new_candidates.push_back(input_candidates[i]);
148 }
149 }
150 const int new_candidate_size = new_candidates.size();
151
152 // Add the selected candidate to the current clique.
153 current_clique->push_back(selected);
154
155 // If there are no remaining candidates, we have found a maximal clique.
156 // Otherwise, do the recursive step.
157 if (new_candidate_size == 0) {
158 *stop = callback(*current_clique);
159 } else {
160 if (new_first_candidate_index < new_candidate_size) {
161 Search(graph, callback, new_candidates.data(),
162 new_first_candidate_index, new_candidate_size, current_clique,
163 stop);
164 if (*stop) {
165 return;
166 }
167 }
168 }
169
170 // Remove the selected candidate from the current clique.
171 current_clique->pop_back();
172 // Add the selected candidate to the set "not" - we've already processed
173 // all possible maximal cliques that use this node in 'current_clique'. The
174 // current candidate is the element of the new candidate set, so we can move
175 // it to "not" simply by increasing first_candidate_index.
176 first_candidate_index++;
177
178 // Find the next candidate that is disconnected from the pivot.
179 if (remaining_candidates > 1) {
180 disconnected_node = first_candidate_index;
181 while (disconnected_node < num_input_candidates &&
182 Connects(graph, pivot, input_candidates[disconnected_node])) {
183 disconnected_node++;
184 }
185 }
186 }
187}
188
189class FindAndEliminate {
190 public:
191 FindAndEliminate(std::function<bool(int, int)> graph, int node_count,
192 std::function<bool(const std::vector<int>&)> callback)
193 : graph_(std::move(graph)),
194 node_count_(node_count),
195 callback_(std::move(callback)) {}
196
197 bool GraphCallback(int node1, int node2) {
198 if (visited_.find(
199 std::make_pair(std::min(node1, node2), std::max(node1, node2))) !=
200 visited_.end()) {
201 return false;
202 }
203 return Connects(graph_, node1, node2);
204 }
205
206 bool SolutionCallback(const std::vector<int>& solution) {
207 const int size = solution.size();
208 if (size > 1) {
209 for (int i = 0; i < size - 1; ++i) {
210 for (int j = i + 1; j < size; ++j) {
211 visited_.insert(std::make_pair(std::min(solution[i], solution[j]),
212 std::max(solution[i], solution[j])));
213 }
214 }
215 callback_(solution);
216 }
217 return false;
218 }
219
220 private:
221 std::function<bool(int, int)> graph_;
222 int node_count_;
223 std::function<bool(const std::vector<int>&)> callback_;
224 absl::flat_hash_set<std::pair<int, int>> visited_;
225};
226} // namespace
227
228// This method implements the 'version2' of the Bron-Kerbosch
229// algorithm to find all maximal cliques in a undirected graph.
230void FindCliques(std::function<bool(int, int)> graph, int node_count,
231 std::function<bool(const std::vector<int>&)> callback) {
232 std::unique_ptr<int[]> initial_candidates(new int[node_count]);
233 std::vector<int> actual;
234
235 for (int c = 0; c < node_count; ++c) {
236 initial_candidates[c] = c;
237 }
238
239 bool stop = false;
240 Search(std::move(graph), std::move(callback), initial_candidates.get(), 0,
241 node_count, &actual, &stop);
242}
243
244void CoverArcsByCliques(std::function<bool(int, int)> graph, int node_count,
245 std::function<bool(const std::vector<int>&)> callback) {
246 FindAndEliminate cache(std::move(graph), node_count, std::move(callback));
247 std::unique_ptr<int[]> initial_candidates(new int[node_count]);
248 std::vector<int> actual;
249
250 std::function<bool(int, int)> cached_graph = [&cache](int i, int j) {
251 return cache.GraphCallback(i, j);
252 };
253 std::function<bool(const std::vector<int>&)> cached_callback =
254 [&cache](const std::vector<int>& res) {
255 return cache.SolutionCallback(res);
256 };
257
258 for (int c = 0; c < node_count; ++c) {
259 initial_candidates[c] = c;
260 }
261
262 bool stop = false;
263 Search(std::move(cached_graph), std::move(cached_callback),
264 initial_candidates.get(), 0, node_count, &actual, &stop);
265}
266
268 work_ = 0;
269 weights_.assign(num_nodes, 0.0);
270
271 // We need +1 in case the graph is complete and form a clique.
272 clique_.resize(num_nodes + 1);
273 clique_weight_.resize(num_nodes + 1);
274 left_to_process_.resize(num_nodes + 1);
275 x_.resize(num_nodes + 1);
276
277 // Initialize to empty graph.
278 graph_.resize(num_nodes);
279 for (Bitset64<int>& bitset : graph_) {
280 bitset.ClearAndResize(num_nodes);
281 }
282}
283
286 // We use Floyd-Warshall algorithm.
287 const int num_nodes = weights_.size();
288 for (int k = 0; k < num_nodes; ++k) {
289 // Loop over all the i => k, we can do that by looking at the not(k) =>
290 // not(i).
291 for (const int i : graph_[k ^ 1]) {
292 // Now i also implies all the literals implied by k.
293 graph_[i].Union(graph_[k]);
294 }
295 }
296}
297
298std::vector<std::vector<int>> WeightedBronKerboschBitsetAlgorithm::Run() {
299 clique_index_and_weight_.clear();
300 std::vector<std::vector<int>> cliques;
301
302 const int num_nodes = weights_.size();
303 in_clique_.ClearAndResize(num_nodes);
304
305 queue_.clear();
306
307 int depth = 0;
308 left_to_process_[0].ClearAndResize(num_nodes);
309 x_[0].ClearAndResize(num_nodes);
310 for (int i = 0; i < num_nodes; ++i) {
311 left_to_process_[0].Set(i);
312 queue_.push_back(i);
313 }
314
315 // We run an iterative DFS where we push all possible next node to
316 // queue_. We just abort brutally if we hit the work limit.
317 while (!queue_.empty() && work_ <= work_limit_) {
318 const int node = queue_.back();
319 if (!in_clique_[node]) {
320 // We add this node to the clique.
321 in_clique_.Set(node);
322 clique_[depth] = node;
323 left_to_process_[depth].Clear(node);
324 x_[depth].Set(node);
325
326 // Note that it might seems we don't need to keep both set since we
327 // only process nodes in order, but because of the pivot optim, while
328 // both set are sorted, they can be "interleaved".
329 ++depth;
330 work_ += num_nodes;
331 const double current_weight = weights_[node] + clique_weight_[depth - 1];
332 clique_weight_[depth] = current_weight;
333 left_to_process_[depth].SetToIntersectionOf(left_to_process_[depth - 1],
334 graph_[node]);
335 x_[depth].SetToIntersectionOf(x_[depth - 1], graph_[node]);
336
337 // Choose a pivot. We use the vertex with highest weight according to:
338 // Samuel Souza Britoa, Haroldo Gambini Santosa, "Preprocessing and
339 // Cutting Planes with Conflict Graphs",
340 // https://arxiv.org/pdf/1909.07780
341 // but maybe random is more robust?
342 int pivot = -1;
343 double pivot_weight = -1.0;
344 for (const int candidate : x_[depth]) {
345 const double candidate_weight = weights_[candidate];
346 if (candidate_weight > pivot_weight) {
347 pivot = candidate;
348 pivot_weight = candidate_weight;
349 }
350 }
351 double total_weight_left = 0.0;
352 for (const int candidate : left_to_process_[depth]) {
353 const double candidate_weight = weights_[candidate];
354 if (candidate_weight > pivot_weight) {
355 pivot = candidate;
356 pivot_weight = candidate_weight;
357 }
358 total_weight_left += candidate_weight;
359 }
360
361 // Heuristic: We can abort early if there is no way to reach the
362 // threshold here.
363 if (current_weight + total_weight_left < weight_threshold_) {
364 continue;
365 }
366
367 if (pivot == -1 && current_weight >= weight_threshold_) {
368 // This clique is maximal.
369 clique_index_and_weight_.push_back({cliques.size(), current_weight});
370 cliques.emplace_back(clique_.begin(), clique_.begin() + depth);
371 continue;
372 }
373
374 for (const int next : left_to_process_[depth]) {
375 if (graph_[pivot][next]) continue; // skip.
376 queue_.push_back(next);
377 }
378 } else {
379 // We finished exploring node.
380 // backtrack.
381 --depth;
382 DCHECK_GE(depth, 0);
383 DCHECK_EQ(clique_[depth], node);
384 in_clique_.Clear(node);
385 queue_.pop_back();
386 }
387 }
388
389 return cliques;
390}
391
392} // namespace operations_research
---------------— Search class --------------—
std::vector< std::vector< int > > Run()
Definition cliques.cc:298
In SWIG mode, we don't want anything besides these top-level includes.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
void FindCliques(std::function< bool(int, int)> graph, int node_count, std::function< bool(const std::vector< int > &)> callback)
Definition cliques.cc:230
void CoverArcsByCliques(std::function< bool(int, int)> graph, int node_count, std::function< bool(const std::vector< int > &)> callback)
Definition cliques.cc:244