From 5c61369a0218b1e2f661dd7bdee3879fa1232e6c Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Mon, 23 Dec 2024 23:53:03 +0100 Subject: [PATCH] feat: implement Bron-Kerbosch algorithm to find maximal cliques in a graph. --- src/lib.rs | 3 ++ src/undirected/cliques.rs | 108 ++++++++++++++++++++++++++++++++++++++ src/undirected/mod.rs | 1 + tests/cliques.rs | 49 +++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 src/undirected/cliques.rs create mode 100644 tests/cliques.rs diff --git a/src/lib.rs b/src/lib.rs index 435f0708..356c3eff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ //! - [connected components](undirected/connected_components/index.html): find disjoint connected sets of vertices ([⇒ Wikipedia][Connected components]) //! - [Kruskal](undirected/kruskal/index.html): find a minimum-spanning-tree ([⇒ Wikipedia][Kruskal]) //! - [Prim](undirected/prim/index.html): find a minimum-spanning-tree ([⇒ Wikipedia][Prim]) +//! - [cliques]: find maximum cliques in a graph ([= Wikipedia][BronKerbosch]) //! //! ### Matching //! @@ -80,6 +81,7 @@ //! [A*]: https://en.wikipedia.org/wiki/A*_search_algorithm //! [BFS]: https://en.wikipedia.org/wiki/Breadth-first_search //! [Brent]: https://en.wikipedia.org/wiki/Cycle_detection#Brent's_algorithm +//! [BronKerbosch]: https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm //! [Connected components]: https://en.wikipedia.org/wiki/Connected_component_(graph_theory) //! [DFS]: https://en.wikipedia.org/wiki/Depth-first_search //! [Dijkstra]: https://en.wikipedia.org/wiki/Dijkstra's_algorithm @@ -131,6 +133,7 @@ pub mod prelude { pub use crate::grid::*; pub use crate::kuhn_munkres::*; pub use crate::matrix::*; + pub use crate::undirected::cliques::*; pub use crate::undirected::connected_components::*; pub use crate::undirected::kruskal::*; pub use crate::utils::*; diff --git a/src/undirected/cliques.rs b/src/undirected/cliques.rs new file mode 100644 index 00000000..f0366e9c --- /dev/null +++ b/src/undirected/cliques.rs @@ -0,0 +1,108 @@ +//! Find cliques in an undirected graph. + +use std::collections::HashSet; +use std::hash::Hash; + +/// Algorithm for finding all maximal cliques in an undirected graph. +/// That is, it lists all subsets of vertices with the two properties that each pair of vertices in +/// one of the listed subsets is connected by an edge, and no listed subset can have +/// any additional vertices added to it while preserving its complete connectivity. +/// [Bron-Kerbosch algorithm](https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm). +/// +/// +/// - `vertices` is the list of all nodes. +/// - `connected` returns true if the two given node is connected. +/// - return a list of cliques. +pub fn maximal_cliques_collect(vertices: IN, connected: &mut FN) -> Vec> +where + N: Eq + Hash + Clone, + FN: FnMut(&N, &N) -> bool, + IN: IntoIterator, +{ + let mut result = Vec::new(); + let mut consumer = |n: &HashSet| result.push(n.to_owned()); + let mut remaining_nodes: HashSet = vertices.into_iter().collect::>(); + bron_kerbosch( + connected, + &HashSet::new(), + &mut remaining_nodes, + &mut HashSet::new(), + &mut consumer, + ); + result +} + +/// Algorithm for finding all maximal cliques in an undirected graph. +/// That is, it lists all subsets of vertices with the two properties that each pair of vertices in +/// one of the listed subsets is connected by an edge, and no listed subset can have +/// any additional vertices added to it while preserving its complete connectivity. +/// [Bron-Kerbosch algorithm](https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm). +/// +/// +/// - `vertices` is the list of all nodes. +/// - `connected` returns true if the two given node is connected. +/// - 'consumer' function which called for each clique. +/// +pub fn maximal_cliques(vertices: IN, connected: &mut FN, consumer: &mut CO) +where + N: Eq + Hash + Clone, + FN: FnMut(&N, &N) -> bool, + IN: IntoIterator, + CO: FnMut(&HashSet), +{ + let mut remaining_nodes: HashSet = vertices.into_iter().collect(); + bron_kerbosch( + connected, + &HashSet::new(), + &mut remaining_nodes, + &mut HashSet::new(), + consumer, + ); +} + +fn bron_kerbosch( + connected: &mut FN, + potential_clique: &HashSet, + remaining_nodes: &mut HashSet, + skip_nodes: &mut HashSet, + consumer: &mut CO, +) where + N: Eq + Hash + Clone, + FN: FnMut(&N, &N) -> bool, + CO: FnMut(&HashSet), +{ + if remaining_nodes.is_empty() && skip_nodes.is_empty() { + consumer(potential_clique); + return; + } + let nodes_to_check = remaining_nodes.clone(); + for node in &nodes_to_check { + let mut new_potential_clique = potential_clique.clone(); + new_potential_clique.insert(node.to_owned()); + + let mut new_remaining_nodes: HashSet = remaining_nodes + .iter() + .filter(|n| *n != node && connected(node, n)) + .cloned() + .collect(); + + let mut new_skip_list: HashSet = skip_nodes + .iter() + .filter(|n| *n != node && connected(node, n)) + .cloned() + .collect(); + bron_kerbosch( + connected, + &new_potential_clique, + &mut new_remaining_nodes, + &mut new_skip_list, + consumer, + ); + + // We're done considering this node. If there was a way to form a clique with it, we + // already discovered its maximal clique in the recursive call above. So, go ahead + // and remove it from the list of remaining nodes and add it to the skip list. + remaining_nodes.remove(node); + skip_nodes.insert(node.to_owned()); + } +} diff --git a/src/undirected/mod.rs b/src/undirected/mod.rs index 05dae374..4849996e 100644 --- a/src/undirected/mod.rs +++ b/src/undirected/mod.rs @@ -1,5 +1,6 @@ //! Algorithms for undirected graphs. +pub mod cliques; pub mod connected_components; pub mod kruskal; pub mod prim; diff --git a/tests/cliques.rs b/tests/cliques.rs new file mode 100644 index 00000000..9faba56e --- /dev/null +++ b/tests/cliques.rs @@ -0,0 +1,49 @@ +use std::collections::HashSet; + +use itertools::Itertools; +use pathfinding::prelude::*; + +#[test] +fn find_cliques() { + let vertices: Vec = (1..10).collect_vec(); + let cliques = maximal_cliques_collect(&vertices, &mut |a, b| (*a - *b) % 3 == 0); + let cliques_as_vectors: Vec> = sort(&cliques); + + assert_eq!( + vec![vec![1, 4, 7], vec![2, 5, 8], vec![3, 6, 9]], + cliques_as_vectors + ); +} + +#[test] +fn test_same_node_appears_in_multiple_clique() { + let vertices: Vec = (1..10).collect_vec(); + let cliques = maximal_cliques_collect(&vertices, &mut |a, b| { + (*a % 3 == 0) && (*b % 3 == 0) || ((*a - *b) % 4 == 0) + }); + let cliques_as_vectors: Vec> = sort(&cliques); + + assert_eq!( + vec![ + vec![1, 5, 9], + vec![2, 6], + vec![3, 6, 9], + vec![3, 7], + vec![4, 8] + ], + cliques_as_vectors + ); +} + +fn sort(cliques: &[HashSet<&i32>]) -> Vec> { + let mut cliques_as_vectors: Vec> = cliques + .iter() + .map(|cliq| { + let mut s = cliq.iter().map(|&x| *x).collect_vec(); + s.sort_unstable(); + s + }) + .collect(); + cliques_as_vectors.sort(); + cliques_as_vectors +}