// Copyright 2010-2018 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. // // Maximal clique algorithms, based on the Bron-Kerbosch algorithm. // See http://en.wikipedia.org/wiki/Bron-Kerbosch_algorithm // and // C. Bron and J. Kerbosch, Joep, "Algorithm 457: finding all cliques of an // undirected graph", CACM 16 (9): 575-577, 1973. // http://dl.acm.org/citation.cfm?id=362367&bnc=1. // // Keywords: undirected graph, clique, clique cover, Bron, Kerbosch. #ifndef OR_TOOLS_GRAPH_CLIQUES_H_ #define OR_TOOLS_GRAPH_CLIQUES_H_ #include #include #include #include "absl/strings/str_cat.h" #include "ortools/base/int_type.h" #include "ortools/base/int_type_indexed_vector.h" #include "ortools/base/logging.h" #include "ortools/util/time_limit.h" namespace operations_research { // Finds all maximal cliques, even of size 1, in the // graph described by the graph callback. graph->Run(i, j) indicates // if there is an arc between i and j. // This function takes ownership of 'callback' and deletes it after it has run. // If 'callback' returns true, then the search for cliques stops. void FindCliques(std::function graph, int node_count, std::function&)> callback); // Covers the maximum number of arcs of the graph with cliques. The graph // is described by the graph callback. graph->Run(i, j) indicates if // there is an arc between i and j. // This function takes ownership of 'callback' and deletes it after it has run. // It calls 'callback' upon each clique. // It ignores cliques of size 1. void CoverArcsByCliques(std::function graph, int node_count, std::function&)> callback); // Possible return values of the callback for reporting cliques. The returned // value determines whether the algorithm will continue the search. enum class CliqueResponse { // The algorithm will continue searching for other maximal cliques. CONTINUE, // The algorithm will stop the search immediately. The search can be resumed // by calling BronKerboschAlgorithm::Run (resp. RunIterations) again. STOP }; // The status value returned by BronKerboschAlgorithm::Run and // BronKerboschAlgorithm::RunIterations. enum class BronKerboschAlgorithmStatus { // The algorithm has enumerated all maximal cliques. COMPLETED, // The search algorithm was interrupted either because it reached the // iteration limit or because the clique callback returned // CliqueResponse::STOP. INTERRUPTED }; // Implements the Bron-Kerbosch algorithm for finding maximal cliques. // The graph is represented as a callback that gets two nodes as its arguments // and it returns true if and only if there is an arc between the two nodes. The // cliques are reported back to the user using a second callback. // // Typical usage: // auto graph = [](int node1, int node2) { return true; }; // auto on_clique = [](const std::vector& clique) { // LOG(INFO) << "Clique!"; // }; // // BronKerboschAlgorithm bron_kerbosch(graph, num_nodes, on_clique); // bron_kerbosch.Run(); // // or: // // BronKerboschAlgorithm bron_kerbosch(graph, num_nodes, clique); // bron_kerbosch.RunIterations(kMaxNumIterations); // // This is a non-recursive implementation of the Bron-Kerbosch algorithm with // pivots as described in the paper by Bron and Kerbosch (1973) (the version 2 // algorithm in the paper). // The basic idea of the algorithm is to incrementally build the cliques using // depth-first search. During the search, the algorithm maintains two sets of // candidates (nodes that are connected to all nodes in the current clique): // - the "not" set - these are candidates that were already visited by the // search and all the maximal cliques that contain them as a part of the // current clique were already reported. // - the actual candidates - these are candidates that were not visited yet, and // they can be added to the clique. // In each iteration, the algorithm does the first of the following actions that // applies: // A. If there are no actual candidates and there are candidates in the "not" // set, or if all actual candidates are connected to the same node in the // "not" set, the current clique can't be extended to a maximal clique that // was not already reported. Return from the recursive call and move the // selected candidate to the set "not". // B. If there are no candidates at all, it means that the current clique can't // be extended and that it is in fact a maximal clique. Report it to the user // and return from the recursive call. Move the selected candidate to the set // "not". // C. Otherwise, there are actual candidates, extend the current clique with one // of these candidates and process it recursively. // // To avoid unnecessary steps, the algorithm selects a pivot at each level of // the recursion to guide the selection of candidates added to the current // clique. The pivot can be either in the "not" set and among the actual // candidates. The algorithm tries to move the pivot and all actual candidates // connected to it to the set "not" as quickly as possible. This will fulfill // the conditions of step A, and the search algorithm will be able to leave the // current branch. Selecting a pivot that has the lowest number of disconnected // nodes among the candidates can reduce the running time significantly. // // The worst-case maximal depth of the recursion is equal to the number of nodes // in the graph, which makes the natural recursive implementation impractical // for nodes with more than a few thousands of nodes. To avoid the limitation, // this class simulates the recursion by maintaining a stack with the state at // each level of the recursion. The algorithm then runs in a loop. In each // iteration, the algorithm can do one or both of: // 1. Return to the previous recursion level (step A or B of the algorithm) by // removing the top state from the stack. // 2. Select the next candidate and enter the next recursion level (step C of // the algorithm) by adding a new state to the stack. // // The worst-case time complexity of the algorithm is O(3^(N/3)), and the memory // complexity is O(N^2), where N is the number of nodes in the graph. template class BronKerboschAlgorithm { public: // A callback called by the algorithm to test if there is an arc between a // pair of nodes. The callback must return true if and only if there is an // arc. Note that to function properly, the function must be symmetrical // (represent an undirected graph). using IsArcCallback = std::function; // A callback called by the algorithm to report a maximal clique to the user. // The clique is returned as a list of nodes in the clique, in no particular // order. The caller must make a copy of the vector if they want to keep the // nodes. // // The return value of the callback controls how the algorithm continues after // this clique. See the description of the values of 'CliqueResponse' for more // details. using CliqueCallback = std::function&)>; // Initializes the Bron-Kerbosch algorithm for the given graph and clique // callback function. BronKerboschAlgorithm(IsArcCallback is_arc, NodeIndex num_nodes, CliqueCallback clique_callback) : is_arc_(std::move(is_arc)), clique_callback_(std::move(clique_callback)), num_nodes_(num_nodes) {} // Runs the Bron-Kerbosch algorithm for kint64max iterations. In practice, // this is equivalent to running until completion or until the clique callback // returns BronKerboschAlgorithmStatus::STOP. If the method returned because // the search is finished, it will return COMPLETED; otherwise, it will return // INTERRUPTED and it can be resumed by calling this method again. BronKerboschAlgorithmStatus Run(); // Runs at most 'max_num_iterations' iterations of the Bron-Kerbosch // algorithm. When this function returns INTERRUPTED, there is still work to // be done to process all the cliques in the graph. In such case the method // can be called again and it will resume the work where the previous call had // stopped. When it returns COMPLETED any subsequent call to the method will // resume the search from the beginning. BronKerboschAlgorithmStatus RunIterations(int64 max_num_iterations); // Runs at most 'max_num_iterations' iterations of the Bron-Kerbosch // algorithm, until the time limit is exceeded or until all cliques are // enumerated. When this function returns INTERRUPTED, there is still work to // be done to process all the cliques in the graph. In such case the method // can be called again and it will resume the work where the previous call had // stopped. When it returns COMPLETED any subsequent call to the method will // resume the search from the beginning. BronKerboschAlgorithmStatus RunWithTimeLimit(int64 max_num_iterations, TimeLimit* time_limit); // Runs the Bron-Kerbosch algorithm for at most kint64max iterations, until // the time limit is excceded or until all cliques are enumerated. In // practice, running the algorithm for kint64max iterations is equivalent to // running until completion or until the other stopping conditions apply. When // this function returns INTERRUPTED, there is still work to be done to // process all the cliques in the graph. In such case the method can be called // again and it will resume the work where the previous call had stopped. When // it returns COMPLETED any subsequent call to the method will resume the // search from the beginning. BronKerboschAlgorithmStatus RunWithTimeLimit(TimeLimit* time_limit) { return RunWithTimeLimit(kint64max, time_limit); } private: DEFINE_INT_TYPE(CandidateIndex, ptrdiff_t); // A data structure that maintains the variables of one "iteration" of the // search algorithm. These are the variables that would normally be allocated // on the stack in the recursive implementation. // // Note that most of the variables in the structure are explicitly left // uninitialized by the constructor to avoid wasting resources on values that // will be overwritten anyway. Most of the initialization is done in // BronKerboschAlgorithm::InitializeState. struct State { State() {} State(const State& other) : pivot(other.pivot), num_remaining_candidates(other.num_remaining_candidates), candidates(other.candidates), first_candidate_index(other.first_candidate_index), candidate_for_recursion(other.candidate_for_recursion) {} State& operator=(const State& other) { pivot = other.pivot; num_remaining_candidates = other.num_remaining_candidates; candidates = other.candidates; first_candidate_index = other.first_candidate_index; candidate_for_recursion = other.candidate_for_recursion; return *this; } // Moves the first candidate in the state to the "not" set. Assumes that the // first candidate is also the pivot or a candidate disconnected from the // pivot (as done by RunIteration). inline void MoveFirstCandidateToNotSet() { ++first_candidate_index; --num_remaining_candidates; } // Creates a human-readable representation of the current state. std::string DebugString() { std::string buffer; absl::StrAppend(&buffer, "pivot = ", pivot, "\nnum_remaining_candidates = ", num_remaining_candidates, "\ncandidates = ["); for (CandidateIndex i(0); i < candidates.size(); ++i) { if (i > 0) buffer += ", "; absl::StrAppend(&buffer, candidates[i]); } absl::StrAppend( &buffer, "]\nfirst_candidate_index = ", first_candidate_index.value(), "\ncandidate_for_recursion = ", candidate_for_recursion.value()); return buffer; } // The pivot node selected for the given level of the recursion. NodeIndex pivot; // The number of remaining candidates to be explored at the given level of // the recursion; the number is computed as num_disconnected_nodes + // pre_increment in the original algorithm. int num_remaining_candidates; // The list of nodes that are candidates for extending the current clique. // This vector has the format proposed in the paper by Bron-Kerbosch; the // first 'first_candidate_index' elements of the vector represent the // "not" set of nodes that were already visited by the algorithm. The // remaining elements are the actual candidates for extending the current // clique. // NOTE(user): We could store the delta between the iterations; however, // we need to evaluate the impact this would have on the performance. gtl::ITIVector candidates; // The index of the first actual candidate in 'candidates'. This number is // also the number of elements of the "not" set stored at the beginning of // 'candidates'. CandidateIndex first_candidate_index; // The current position in candidates when looking for the pivot and/or the // next candidate disconnected from the pivot. CandidateIndex candidate_for_recursion; }; // The deterministic time coefficients for the push and pop operations of the // Bron-Kerbosch algorithm. The coefficients are set to match approximately // the running time in seconds on a recent workstation on the random graph // benchmark. // NOTE(user): PushState is not the only source of complexity in the // algorithm, but non-negative linear least squares produced zero coefficients // for all other deterministic counters tested during the benchmarking. When // we optimize the algorithm, we might need to add deterministic time to the // other places that may produce complexity, namely InitializeState, PopState // and SelectCandidateIndexForRecursion. static const double kPushStateDeterministicTimeSecondsPerCandidate; // Initializes the root state of the algorithm. void Initialize(); // Removes the top state from the state stack. This is equivalent to returning // in the recursive implementation of the algorithm. void PopState(); // Adds a new state to the top of the stack, adding the node 'selected' to the // current clique. This is equivalent to making a recurisve call in the // recursive implementation of the algorithm. void PushState(NodeIndex selected); // Initializes the given state. Runs the pivot selection algorithm in the // state. void InitializeState(State* state); // Returns true if (node1, node2) is an arc in the graph or if node1 == node2. inline bool IsArc(NodeIndex node1, NodeIndex node2) const { return node1 == node2 || is_arc_(node1, node2); } // Selects the next node for recursion. The selected node is either the pivot // (if it is not in the set "not") or a node that is disconnected from the // pivot. CandidateIndex SelectCandidateIndexForRecursion(State* state); // Returns a human-readable string representation of the clique. std::string CliqueDebugString(const std::vector& clique); // The callback called when the algorithm needs to determine if (node1, node2) // is an arc in the graph. IsArcCallback is_arc_; // The callback called when the algorithm discovers a maximal clique. The // return value of the callback controls how the algorithm proceeds with the // clique search. CliqueCallback clique_callback_; // The number of nodes in the graph. const NodeIndex num_nodes_; // Contains the state of the aglorithm. The vector serves as an external stack // for the recursive part of the algorithm - instead of using the C++ stack // and natural recursion, it is implemented as a loop and new states are added // to the top of the stack. The algorithm ends when the stack is empty. std::vector states_; // A vector that receives the current clique found by the algorithm. std::vector current_clique_; // Set to true if the algorithm is active (it was not stopped by an the clique // callback). int64 num_remaining_iterations_; // The current time limit used by the solver. The time limit is assigned by // the Run methods and it can be different for each call to run. TimeLimit* time_limit_; }; template void BronKerboschAlgorithm::InitializeState(State* state) { DCHECK(state != nullptr); const int num_candidates = state->candidates.size(); int num_disconnected_candidates = num_candidates; state->pivot = 0; CandidateIndex pivot_index(-1); for (CandidateIndex pivot_candidate_index(0); pivot_candidate_index < num_candidates && num_disconnected_candidates > 0; ++pivot_candidate_index) { const NodeIndex pivot_candidate = state->candidates[pivot_candidate_index]; int count = 0; for (CandidateIndex i(state->first_candidate_index); i < num_candidates; ++i) { if (!IsArc(pivot_candidate, state->candidates[i])) { ++count; } } if (count < num_disconnected_candidates) { pivot_index = pivot_candidate_index; state->pivot = pivot_candidate; num_disconnected_candidates = count; } } state->num_remaining_candidates = num_disconnected_candidates; if (pivot_index >= state->first_candidate_index) { std::swap(state->candidates[pivot_index], state->candidates[state->first_candidate_index]); ++state->num_remaining_candidates; } } template typename BronKerboschAlgorithm::CandidateIndex BronKerboschAlgorithm::SelectCandidateIndexForRecursion( State* state) { DCHECK(state != nullptr); CandidateIndex disconnected_node_index = std::max(state->first_candidate_index, state->candidate_for_recursion); while (disconnected_node_index < state->candidates.size() && state->candidates[disconnected_node_index] != state->pivot && IsArc(state->pivot, state->candidates[disconnected_node_index])) { ++disconnected_node_index; } state->candidate_for_recursion = disconnected_node_index; return disconnected_node_index; } template void BronKerboschAlgorithm::Initialize() { DCHECK(states_.empty()); states_.reserve(num_nodes_); states_.emplace_back(); State* const root_state = &states_.back(); root_state->first_candidate_index = 0; root_state->candidate_for_recursion = 0; root_state->candidates.resize(num_nodes_, 0); std::iota(root_state->candidates.begin(), root_state->candidates.end(), 0); root_state->num_remaining_candidates = num_nodes_; InitializeState(root_state); DVLOG(2) << "Initialized"; } template void BronKerboschAlgorithm::PopState() { DCHECK(!states_.empty()); states_.pop_back(); if (!states_.empty()) { State* const state = &states_.back(); current_clique_.pop_back(); state->MoveFirstCandidateToNotSet(); } } template std::string BronKerboschAlgorithm::CliqueDebugString( const std::vector& clique) { std::string message = "Clique: [ "; for (const NodeIndex node : clique) { absl::StrAppend(&message, node, " "); } message += "]"; return message; } template void BronKerboschAlgorithm::PushState(NodeIndex selected) { DCHECK(!states_.empty()); DCHECK(time_limit_ != nullptr); DVLOG(2) << "PushState: New depth = " << states_.size() + 1 << ", selected node = " << selected; gtl::ITIVector new_candidates; State* const previous_state = &states_.back(); const double deterministic_time = kPushStateDeterministicTimeSecondsPerCandidate * previous_state->candidates.size(); time_limit_->AdvanceDeterministicTime(deterministic_time, "PushState"); // Add all candidates from previous_state->candidates that are connected to // 'selected' in the graph to the vector 'new_candidates', skipping the node // 'selected'; this node is always at the position // 'previous_state->first_candidate_index', so we can skip it by skipping the // element at this particular index. new_candidates.reserve(previous_state->candidates.size()); for (CandidateIndex i(0); i < previous_state->first_candidate_index; ++i) { const NodeIndex candidate = previous_state->candidates[i]; if (IsArc(selected, candidate)) { new_candidates.push_back(candidate); } } const CandidateIndex new_first_candidate_index(new_candidates.size()); for (CandidateIndex i = previous_state->first_candidate_index + 1; i < previous_state->candidates.size(); ++i) { const NodeIndex candidate = previous_state->candidates[i]; if (IsArc(selected, candidate)) { new_candidates.push_back(candidate); } } current_clique_.push_back(selected); if (new_candidates.empty()) { // We've found a clique. Report it to the user, but do not push the state // because it would be popped immediately anyway. DVLOG(2) << CliqueDebugString(current_clique_); const CliqueResponse response = clique_callback_(current_clique_); if (response == CliqueResponse::STOP) { // The number of remaining iterations will be decremented at the end of // the loop in RunIterations; setting it to 0 here would make it -1 at // the end of the main loop. num_remaining_iterations_ = 1; } current_clique_.pop_back(); previous_state->MoveFirstCandidateToNotSet(); return; } // NOTE(user): The following line may invalidate previous_state (if the // vector data was re-allocated in the process). We must avoid using // previous_state below here. states_.emplace_back(); State* const new_state = &states_.back(); new_state->candidates.swap(new_candidates); new_state->first_candidate_index = new_first_candidate_index; InitializeState(new_state); } template BronKerboschAlgorithmStatus BronKerboschAlgorithm::RunWithTimeLimit( int64 max_num_iterations, TimeLimit* time_limit) { CHECK(time_limit != nullptr); time_limit_ = time_limit; if (states_.empty()) { Initialize(); } for (num_remaining_iterations_ = max_num_iterations; !states_.empty() && num_remaining_iterations_ > 0 && !time_limit->LimitReached(); --num_remaining_iterations_) { State* const state = &states_.back(); DVLOG(2) << "Loop: " << states_.size() << " states, " << state->num_remaining_candidates << " candidate to explore\n" << state->DebugString(); if (state->num_remaining_candidates == 0) { PopState(); continue; } const CandidateIndex selected_index = SelectCandidateIndexForRecursion(state); DVLOG(2) << "selected_index = " << selected_index; const NodeIndex selected = state->candidates[selected_index]; DVLOG(2) << "Selected candidate = " << selected; NodeIndex& f = state->candidates[state->first_candidate_index]; NodeIndex& s = state->candidates[selected_index]; std::swap(f, s); PushState(selected); } time_limit_ = nullptr; return states_.empty() ? BronKerboschAlgorithmStatus::COMPLETED : BronKerboschAlgorithmStatus::INTERRUPTED; } template BronKerboschAlgorithmStatus BronKerboschAlgorithm::RunIterations( int64 max_num_iterations) { TimeLimit time_limit(std::numeric_limits::infinity()); return RunWithTimeLimit(max_num_iterations, &time_limit); } template BronKerboschAlgorithmStatus BronKerboschAlgorithm::Run() { return RunIterations(kint64max); } template const double BronKerboschAlgorithm< NodeIndex>::kPushStateDeterministicTimeSecondsPerCandidate = 0.54663e-7; } // namespace operations_research #endif // OR_TOOLS_GRAPH_CLIQUES_H_