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