Skip to main content

rust_igraph/algorithms/properties/
clique_cover.rs

1//! Clique cover (ALGO-TR-032).
2//!
3//! A **clique cover** (or clique partition) partitions the vertices of
4//! a graph into cliques. The **clique cover number** `θ(G)` is the
5//! minimum number of cliques needed.
6//!
7//! - `is_clique_cover`: validate a partition.
8//! - `greedy_clique_cover`: heuristic greedy cover.
9//! - `clique_cover_number`: brute-force minimum (small graphs only).
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20
21/// Check whether a partition of vertices forms a valid clique cover.
22///
23/// Each part must be a clique in the graph, and every vertex must
24/// appear exactly once.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, is_clique_cover};
30///
31/// let g = Graph::from_edges(
32///     &[(0,1),(0,2),(1,2),(3,4)], false, Some(5)
33/// ).unwrap();
34/// assert!(is_clique_cover(&g, &[vec![0,1,2], vec![3,4]]).unwrap());
35/// assert!(!is_clique_cover(&g, &[vec![0,1,3], vec![2,4]]).unwrap());
36/// ```
37pub fn is_clique_cover(graph: &Graph, parts: &[Vec<u32>]) -> IgraphResult<bool> {
38    let n = graph.vcount() as usize;
39    let mut seen = vec![false; n];
40
41    let adj = build_adj(graph, n);
42
43    for part in parts {
44        for &v in part {
45            let vi = v as usize;
46            if vi >= n || seen[vi] {
47                return Ok(false);
48            }
49            seen[vi] = true;
50        }
51
52        for i in 0..part.len() {
53            for j in (i + 1)..part.len() {
54                let ui = part[i] as usize;
55                let vi = part[j] as usize;
56                if !adj[ui * n + vi] {
57                    return Ok(false);
58                }
59            }
60        }
61    }
62
63    if seen.iter().any(|&s| !s) {
64        return Ok(false);
65    }
66
67    Ok(true)
68}
69
70/// Find a greedy clique cover.
71///
72/// Iterates through vertices and greedily adds each to the first
73/// existing clique it can extend, or starts a new clique.
74///
75/// Returns a partition of vertices into cliques.
76///
77/// # Examples
78///
79/// ```
80/// use rust_igraph::{Graph, greedy_clique_cover, is_clique_cover};
81///
82/// let g = Graph::from_edges(
83///     &[(0,1),(0,2),(1,2),(3,4)], false, Some(5)
84/// ).unwrap();
85/// let cover = greedy_clique_cover(&g).unwrap();
86/// assert!(is_clique_cover(&g, &cover).unwrap());
87/// ```
88pub fn greedy_clique_cover(graph: &Graph) -> IgraphResult<Vec<Vec<u32>>> {
89    let n = graph.vcount() as usize;
90    if n == 0 {
91        return Ok(Vec::new());
92    }
93
94    let adj = build_adj(graph, n);
95
96    let mut degrees: Vec<(usize, u32)> = (0..n as u32)
97        .map(|v| {
98            let d = graph.degree(v).unwrap_or(0);
99            (d, v)
100        })
101        .collect();
102    degrees.sort_by_key(|b| std::cmp::Reverse(b.0));
103
104    let mut cliques: Vec<Vec<u32>> = Vec::new();
105
106    for &(_, v) in &degrees {
107        let vi = v as usize;
108        let mut placed = false;
109
110        for clique in &mut cliques {
111            let fits = clique.iter().all(|&u| {
112                let ui = u as usize;
113                adj[vi * n + ui]
114            });
115            if fits {
116                clique.push(v);
117                placed = true;
118                break;
119            }
120        }
121
122        if !placed {
123            cliques.push(vec![v]);
124        }
125    }
126
127    for clique in &mut cliques {
128        clique.sort_unstable();
129    }
130
131    Ok(cliques)
132}
133
134/// Compute the clique cover number `θ(G)`.
135///
136/// The minimum number of cliques needed to cover all vertices.
137/// Uses brute-force search — only feasible for small graphs.
138///
139/// # Examples
140///
141/// ```
142/// use rust_igraph::{Graph, clique_cover_number};
143///
144/// // K_3 + K_2: need exactly 2 cliques
145/// let g = Graph::from_edges(
146///     &[(0,1),(0,2),(1,2),(3,4)], false, Some(5)
147/// ).unwrap();
148/// assert_eq!(clique_cover_number(&g).unwrap(), 2);
149/// ```
150pub fn clique_cover_number(graph: &Graph) -> IgraphResult<u32> {
151    let n = graph.vcount() as usize;
152    if n == 0 {
153        return Ok(0);
154    }
155
156    let adj = build_adj(graph, n);
157
158    let maximal_cliques = find_all_maximal_cliques(&adj, n);
159
160    let mut best = n as u32;
161    cover_search(&maximal_cliques, &mut vec![false; n], 0, 0, &mut best);
162
163    Ok(best)
164}
165
166fn build_adj(graph: &Graph, n: usize) -> Vec<bool> {
167    let mut adj = vec![false; n * n];
168    for (u, v) in graph.edges() {
169        let ui = u as usize;
170        let vi = v as usize;
171        adj[ui * n + vi] = true;
172        if !graph.is_directed() {
173            adj[vi * n + ui] = true;
174        }
175    }
176    adj
177}
178
179fn find_all_maximal_cliques(adj: &[bool], n: usize) -> Vec<Vec<usize>> {
180    let mut cliques = Vec::new();
181    let mut current = Vec::new();
182    let candidates: Vec<usize> = (0..n).collect();
183    let excluded: Vec<usize> = Vec::new();
184    bron_kerbosch(adj, n, &mut current, &candidates, &excluded, &mut cliques);
185    cliques.push(Vec::new());
186    cliques.retain(|c| !c.is_empty());
187
188    for v in 0..n {
189        let mut found = false;
190        for c in &cliques {
191            if c.contains(&v) {
192                found = true;
193                break;
194            }
195        }
196        if !found {
197            cliques.push(vec![v]);
198        }
199    }
200
201    cliques
202}
203
204fn bron_kerbosch(
205    adj: &[bool],
206    n: usize,
207    current: &mut Vec<usize>,
208    candidates: &[usize],
209    excluded: &[usize],
210    results: &mut Vec<Vec<usize>>,
211) {
212    if candidates.is_empty() && excluded.is_empty() {
213        if !current.is_empty() {
214            results.push(current.clone());
215        }
216        return;
217    }
218
219    let mut cand = candidates.to_vec();
220    let mut excl = excluded.to_vec();
221
222    let pivot = cand
223        .iter()
224        .chain(excl.iter())
225        .max_by_key(|&&v| cand.iter().filter(|&&u| adj[v * n + u]).count())
226        .copied();
227
228    let Some(pivot_v) = pivot else { return };
229
230    let to_try: Vec<usize> = cand
231        .iter()
232        .filter(|&&v| !adj[pivot_v * n + v])
233        .copied()
234        .collect();
235
236    for v in to_try {
237        let new_cand: Vec<usize> = cand.iter().filter(|&&u| adj[v * n + u]).copied().collect();
238        let new_excl: Vec<usize> = excl.iter().filter(|&&u| adj[v * n + u]).copied().collect();
239
240        current.push(v);
241        bron_kerbosch(adj, n, current, &new_cand, &new_excl, results);
242        current.pop();
243
244        cand.retain(|&u| u != v);
245        excl.push(v);
246    }
247}
248
249fn cover_search(
250    cliques: &[Vec<usize>],
251    covered: &mut Vec<bool>,
252    depth: u32,
253    start: usize,
254    best: &mut u32,
255) {
256    if covered.iter().all(|&c| c) {
257        if depth < *best {
258            *best = depth;
259        }
260        return;
261    }
262
263    if depth.saturating_add(1) >= *best {
264        return;
265    }
266
267    let uncovered = covered.iter().position(|&c| !c);
268    let Some(target) = uncovered else { return };
269
270    for i in start..cliques.len() {
271        if !cliques[i].contains(&target) {
272            continue;
273        }
274
275        let any_already_uncovered = cliques[i].iter().any(|&v| !covered[v]);
276        if !any_already_uncovered {
277            continue;
278        }
279
280        let mut restored = Vec::new();
281        for &v in &cliques[i] {
282            if !covered[v] {
283                covered[v] = true;
284                restored.push(v);
285            }
286        }
287
288        cover_search(cliques, covered, depth.saturating_add(1), i + 1, best);
289
290        for v in restored {
291            covered[v] = false;
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    fn path4() -> Graph {
301        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
302    }
303
304    fn k3() -> Graph {
305        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
306    }
307
308    fn k4() -> Graph {
309        Graph::from_edges(
310            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
311            false,
312            Some(4),
313        )
314        .unwrap()
315    }
316
317    fn cycle4() -> Graph {
318        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
319    }
320
321    fn cycle5() -> Graph {
322        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
323    }
324
325    fn k3_plus_k2() -> Graph {
326        Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (3, 4)], false, Some(5)).unwrap()
327    }
328
329    fn star5() -> Graph {
330        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
331    }
332
333    // --- is_clique_cover ---
334
335    #[test]
336    fn icc_valid() {
337        let g = k3_plus_k2();
338        assert!(is_clique_cover(&g, &[vec![0, 1, 2], vec![3, 4]]).unwrap());
339    }
340
341    #[test]
342    fn icc_invalid_not_clique() {
343        let g = k3_plus_k2();
344        assert!(!is_clique_cover(&g, &[vec![0, 1, 3], vec![2, 4]]).unwrap());
345    }
346
347    #[test]
348    fn icc_missing_vertex() {
349        let g = k3_plus_k2();
350        assert!(!is_clique_cover(&g, &[vec![0, 1, 2]]).unwrap());
351    }
352
353    #[test]
354    fn icc_duplicate_vertex() {
355        let g = k3_plus_k2();
356        assert!(!is_clique_cover(&g, &[vec![0, 1, 2], vec![2, 3, 4]]).unwrap());
357    }
358
359    #[test]
360    fn icc_singletons() {
361        let g = path4();
362        assert!(is_clique_cover(&g, &[vec![0], vec![1], vec![2], vec![3]]).unwrap());
363    }
364
365    #[test]
366    fn icc_empty() {
367        let g = Graph::with_vertices(0);
368        assert!(is_clique_cover(&g, &[]).unwrap());
369    }
370
371    #[test]
372    fn icc_edges_as_cliques() {
373        let g = path4();
374        assert!(is_clique_cover(&g, &[vec![0, 1], vec![2, 3]]).unwrap());
375    }
376
377    // --- greedy_clique_cover ---
378
379    #[test]
380    fn gcc_empty() {
381        let g = Graph::with_vertices(0);
382        let cover = greedy_clique_cover(&g).unwrap();
383        assert!(cover.is_empty());
384    }
385
386    #[test]
387    fn gcc_isolated() {
388        let g = Graph::with_vertices(3);
389        let cover = greedy_clique_cover(&g).unwrap();
390        assert!(is_clique_cover(&g, &cover).unwrap());
391        assert_eq!(cover.len(), 3);
392    }
393
394    #[test]
395    fn gcc_k3() {
396        let g = k3();
397        let cover = greedy_clique_cover(&g).unwrap();
398        assert!(is_clique_cover(&g, &cover).unwrap());
399        assert_eq!(cover.len(), 1);
400    }
401
402    #[test]
403    fn gcc_k4() {
404        let g = k4();
405        let cover = greedy_clique_cover(&g).unwrap();
406        assert!(is_clique_cover(&g, &cover).unwrap());
407        assert_eq!(cover.len(), 1);
408    }
409
410    #[test]
411    fn gcc_k3_plus_k2() {
412        let g = k3_plus_k2();
413        let cover = greedy_clique_cover(&g).unwrap();
414        assert!(is_clique_cover(&g, &cover).unwrap());
415        assert!(cover.len() <= 3);
416    }
417
418    #[test]
419    fn gcc_path4() {
420        let g = path4();
421        let cover = greedy_clique_cover(&g).unwrap();
422        assert!(is_clique_cover(&g, &cover).unwrap());
423    }
424
425    #[test]
426    fn gcc_cycle5() {
427        let g = cycle5();
428        let cover = greedy_clique_cover(&g).unwrap();
429        assert!(is_clique_cover(&g, &cover).unwrap());
430    }
431
432    #[test]
433    fn gcc_star5() {
434        let g = star5();
435        let cover = greedy_clique_cover(&g).unwrap();
436        assert!(is_clique_cover(&g, &cover).unwrap());
437    }
438
439    // --- clique_cover_number ---
440
441    #[test]
442    fn ccn_empty() {
443        let g = Graph::with_vertices(0);
444        assert_eq!(clique_cover_number(&g).unwrap(), 0);
445    }
446
447    #[test]
448    fn ccn_isolated() {
449        let g = Graph::with_vertices(3);
450        assert_eq!(clique_cover_number(&g).unwrap(), 3);
451    }
452
453    #[test]
454    fn ccn_k3() {
455        assert_eq!(clique_cover_number(&k3()).unwrap(), 1);
456    }
457
458    #[test]
459    fn ccn_k4() {
460        assert_eq!(clique_cover_number(&k4()).unwrap(), 1);
461    }
462
463    #[test]
464    fn ccn_k3_plus_k2() {
465        assert_eq!(clique_cover_number(&k3_plus_k2()).unwrap(), 2);
466    }
467
468    #[test]
469    fn ccn_path4() {
470        assert_eq!(clique_cover_number(&path4()).unwrap(), 2);
471    }
472
473    #[test]
474    fn ccn_cycle4() {
475        assert_eq!(clique_cover_number(&cycle4()).unwrap(), 2);
476    }
477
478    #[test]
479    fn ccn_cycle5() {
480        assert_eq!(clique_cover_number(&cycle5()).unwrap(), 3);
481    }
482
483    #[test]
484    fn ccn_single_vertex() {
485        let g = Graph::with_vertices(1);
486        assert_eq!(clique_cover_number(&g).unwrap(), 1);
487    }
488
489    #[test]
490    fn ccn_single_edge() {
491        let g = Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap();
492        assert_eq!(clique_cover_number(&g).unwrap(), 1);
493    }
494
495    // --- cross-consistency ---
496
497    #[test]
498    fn greedy_at_least_optimal() {
499        for g in &[k3(), k4(), k3_plus_k2(), path4(), cycle4()] {
500            let greedy = greedy_clique_cover(g).unwrap();
501            let opt = clique_cover_number(g).unwrap();
502            assert!(greedy.len() as u32 >= opt);
503        }
504    }
505
506    #[test]
507    fn cover_number_at_most_n() {
508        for g in &[k3(), k4(), path4(), cycle5(), star5()] {
509            let theta = clique_cover_number(g).unwrap();
510            assert!(theta <= g.vcount());
511        }
512    }
513
514    #[test]
515    fn complete_graph_cover_is_one() {
516        for n in 1_u32..=5 {
517            let edges: Vec<(u32, u32)> = (0..n)
518                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
519                .collect();
520            let g = if edges.is_empty() {
521                Graph::with_vertices(n)
522            } else {
523                Graph::from_edges(&edges, false, Some(n)).unwrap()
524            };
525            assert_eq!(clique_cover_number(&g).unwrap(), 1);
526        }
527    }
528}