Skip to main content

uni_algo/algo/algorithms/
maximal_cliques.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Maximal Cliques Algorithm (Bron-Kerbosch with Pivot).
5//!
6//! Finds all maximal cliques in an undirected graph.
7//! A maximal clique is a subgraph where every node is connected to every other node,
8//! and it cannot be extended by adding another node.
9
10use crate::algo::GraphProjection;
11use crate::algo::algorithms::Algorithm;
12use std::collections::HashSet;
13use uni_common::core::id::Vid;
14
15pub struct MaximalCliques;
16
17#[derive(Debug, Clone)]
18pub struct MaximalCliquesConfig {
19    pub min_size: usize,
20}
21
22impl Default for MaximalCliquesConfig {
23    fn default() -> Self {
24        Self { min_size: 2 }
25    }
26}
27
28pub struct MaximalCliquesResult {
29    pub cliques: Vec<Vec<Vid>>,
30}
31
32impl Algorithm for MaximalCliques {
33    type Config = MaximalCliquesConfig;
34    type Result = MaximalCliquesResult;
35
36    fn name() -> &'static str {
37        "maximal_cliques"
38    }
39
40    fn run(graph: &GraphProjection, config: Self::Config) -> Self::Result {
41        let n = graph.vertex_count();
42        if n == 0 {
43            return MaximalCliquesResult {
44                cliques: Vec::new(),
45            };
46        }
47
48        // Convert graph to adjacency sets for fast lookup O(1) or O(d).
49        // GraphProjection is CSR. To check u-v edge, we scan neighbors of u.
50        // For dense graphs, bitset/matrix is better.
51        // For sparse graphs, sorted adjacency list allows O(log d) or intersection.
52        // Bron-Kerbosch mainly iterates neighbors.
53        // P intersection N(u).
54        // GraphProjection neighbors are usually sorted? No guarantee.
55
56        // Treat graph as undirected: u-v exists if u->v OR v->u.
57        // We can build a local adjacency list `adj` where adj[u] contains v if connected.
58
59        let mut adj = vec![HashSet::new(); n];
60        for u in 0..n {
61            for &v_u32 in graph.out_neighbors(u as u32) {
62                let v = v_u32 as usize;
63                if u != v {
64                    adj[u].insert(v);
65                    adj[v].insert(u);
66                }
67            }
68            if graph.has_reverse() {
69                for &v_u32 in graph.in_neighbors(u as u32) {
70                    let v = v_u32 as usize;
71                    if u != v {
72                        adj[u].insert(v);
73                        adj[v].insert(u);
74                    }
75                }
76            }
77        }
78
79        let mut cliques = Vec::new();
80        let r = HashSet::new();
81        let p: HashSet<usize> = (0..n).collect();
82        let x = HashSet::new();
83
84        bron_kerbosch_pivot(&adj, r, p, x, config.min_size, &mut cliques);
85
86        let mapped_cliques = cliques
87            .into_iter()
88            .map(|clique| {
89                clique
90                    .into_iter()
91                    .map(|idx| graph.to_vid(idx as u32))
92                    .collect()
93            })
94            .collect();
95
96        MaximalCliquesResult {
97            cliques: mapped_cliques,
98        }
99    }
100}
101
102fn bron_kerbosch_pivot(
103    adj: &[HashSet<usize>],
104    r: HashSet<usize>,
105    mut p: HashSet<usize>,
106    mut x: HashSet<usize>,
107    min_size: usize,
108    cliques: &mut Vec<Vec<usize>>,
109) {
110    if p.is_empty() && x.is_empty() {
111        if r.len() >= min_size {
112            cliques.push(r.into_iter().collect());
113        }
114        return;
115    }
116
117    // Pivot selection: choose u in P U X that maximizes |P intersect N(u)|
118    let u_pivot = p
119        .union(&x)
120        .max_by_key(|&u| p.iter().filter(|&v| adj[*u].contains(v)).count())
121        .copied();
122
123    // If no pivot (P U X empty), we are done (handled by base case).
124    // If P U X not empty, u_pivot exists.
125    let u = u_pivot.unwrap();
126
127    // P \ N(u)
128    let candidates: Vec<usize> = p.iter().filter(|&v| !adj[u].contains(v)).copied().collect();
129
130    for v in candidates {
131        let mut new_r = r.clone();
132        new_r.insert(v);
133
134        let new_p: HashSet<usize> = p.iter().filter(|&n| adj[v].contains(n)).copied().collect();
135
136        let new_x: HashSet<usize> = x.iter().filter(|&n| adj[v].contains(n)).copied().collect();
137
138        bron_kerbosch_pivot(adj, new_r, new_p, new_x, min_size, cliques);
139
140        p.remove(&v);
141        x.insert(v);
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::algo::test_utils::build_test_graph;
149
150    #[test]
151    fn test_cliques_triangle() {
152        // 0-1, 1-2, 2-0 (Triangle)
153        // Clique: {0, 1, 2}
154        let vids = vec![Vid::from(0), Vid::from(1), Vid::from(2)];
155        let edges = vec![
156            (Vid::from(0), Vid::from(1)),
157            (Vid::from(1), Vid::from(2)),
158            (Vid::from(2), Vid::from(0)),
159        ];
160        let graph = build_test_graph(vids, edges);
161
162        let result = MaximalCliques::run(&graph, MaximalCliquesConfig::default());
163        assert_eq!(result.cliques.len(), 1);
164        assert_eq!(result.cliques[0].len(), 3);
165    }
166
167    #[test]
168    fn test_cliques_two_triangles() {
169        // 0-1-2-0 (Triangle 1)
170        // 2-3-4-2 (Triangle 2)
171        // Joint at 2.
172        // Cliques: {0,1,2}, {2,3,4}
173        let vids = (0..5).map(Vid::from).collect();
174        let edges = vec![
175            (Vid::from(0), Vid::from(1)),
176            (Vid::from(1), Vid::from(2)),
177            (Vid::from(2), Vid::from(0)),
178            (Vid::from(2), Vid::from(3)),
179            (Vid::from(3), Vid::from(4)),
180            (Vid::from(4), Vid::from(2)),
181        ];
182        let graph = build_test_graph(vids, edges);
183
184        let result = MaximalCliques::run(&graph, MaximalCliquesConfig::default());
185        assert_eq!(result.cliques.len(), 2);
186    }
187}