Skip to main content

rust_igraph/algorithms/flow/
vertex_connectivity.rs

1//! `vertex_connectivity` (ALGO-FL-015) — global graph cohesion: the
2//! minimum number of internal vertices whose removal disconnects some
3//! pair of vertices in the graph.
4//!
5//! Counterpart of `igraph_vertex_connectivity` in
6//! `references/igraph/src/flow/flow.c:2158`. Equivalent to the C
7//! alias `igraph_cohesion` (flow.c:2470) and to python-igraph's
8//! `Graph.vertex_connectivity()` (and `Graph.cohesion()`),
9//! R-igraph's `vertex_connectivity()` (no source/target).
10//!
11//! ## Algorithm
12//!
13//! Definition: `vc(G) = min_{s ≠ t} kappa(s, t)` where `kappa(s, t)`
14//! is the s-t vertex connectivity (ALGO-FL-013 in
15//! `IGRAPH_VCONN_NEI_NUMBER_OF_NODES` mode, so that a direct
16//! `s → t` edge contributes the "infinity" sentinel and never
17//! lowers the running minimum).
18//!
19//! Optional cheap short-circuits when `checks = true` (suggested by
20//! Peter `McMahan` per the upstream C docstring, see
21//! flow.c:2147-2149):
22//!
23//! 1. Empty graph → `0` (no pair exists).
24//! 2. Disconnected (weakly for undirected, strongly for directed)
25//!    → `0` (some pair of vertices is already separated).
26//! 3. Any vertex with `min(in, out) = 1` (or undirected degree
27//!    `= 1`) → `1` (removing its single neighbour separates it).
28//! 4. Complete graph (`K_n` or its mutual directed counterpart)
29//!    → `n - 1` (every pair is internally adjacent).
30//!
31//! Otherwise iterate FL-013 over all unordered pairs (undirected) or
32//! ordered pairs (directed), tracking the running minimum and
33//! exiting early once it hits `0`.
34//!
35//! ## Complexity
36//!
37//! `O(V^2)` calls to FL-013, each `O(V·E^2)` on the split-graph
38//! max-flow → `O(V^3·E^2)` worst case. The igraph C docstring
39//! reports `O(|V|^5)` (their bound assumes a denser graph).
40//!
41//! ## Direct-edge handling in the inner loop
42//!
43//! The inner call uses [`VconnNei::NumberOfNodes`] so a direct edge
44//! `s → t` yields `vcount()` (≥ `vcount - 1`). Comparing
45//! `conn < min_conn` (with `min_conn` initialised to `vcount - 1`)
46//! is therefore false for such pairs — they leave `min_conn`
47//! unchanged. This mirrors the upstream C loop at flow.c:1969-2037.
48
49use crate::core::{Graph, IgraphResult};
50
51use super::st_vertex_connectivity::{VconnNei, st_vertex_connectivity};
52use crate::algorithms::connectivity::components::connected_components;
53use crate::algorithms::connectivity::strong::strongly_connected_components;
54use crate::algorithms::properties::is_complete::is_complete;
55
56/// Vertex connectivity (cohesion) of a graph.
57///
58/// Returns the minimum number of internal vertices that must be
59/// removed to disconnect *some* pair of vertices in `graph`. Equal
60/// to `min_{s ≠ t} st_vertex_connectivity(s, t,
61/// VconnNei::NumberOfNodes)`.
62///
63/// Counterpart of `igraph_vertex_connectivity`
64/// (`references/igraph/src/flow/flow.c:2158`) and its alias
65/// `igraph_cohesion` (flow.c:2470).
66///
67/// # Arguments
68///
69/// * `graph` — input graph (directed or undirected).
70/// * `checks` — when `true`, run the cheap short-circuits described
71///   in the module docs before falling back to the `O(V^2)` pairwise
72///   loop. Recommended for any non-trivial graph; the helpers cost
73///   `O(V + E)` and can replace the whole pairwise loop. Equivalent
74///   to igraph C's `checks` argument.
75///
76/// # Returns
77///
78/// The vertex connectivity as `i64`. Returns:
79/// * `0` when `vcount() < 2`, or the graph is disconnected (some
80///   pair is already separated).
81/// * `1` when there is a vertex with degree `1` (undirected) or
82///   `min(in, out) = 1` (directed).
83/// * `vcount - 1` when the graph is complete.
84/// * Otherwise, the result of the pairwise FL-013 loop.
85///
86/// # Errors
87///
88/// Propagates errors from [`st_vertex_connectivity`],
89/// [`connected_components`], [`strongly_connected_components`], and
90/// [`is_complete`]. In practice these arise only from arithmetic
91/// overflow on very large graphs, which is unreachable here.
92///
93/// [`IgraphError`]: crate::core::IgraphError
94///
95/// # Examples
96///
97/// ```
98/// use rust_igraph::{Graph, vertex_connectivity};
99///
100/// // Undirected ring on 5 vertices — vc = 2 (any single vertex
101/// // removal leaves the rest connected).
102/// let mut g = Graph::new(5, false).unwrap();
103/// for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)] {
104///     g.add_edge(u, v).unwrap();
105/// }
106/// assert_eq!(vertex_connectivity(&g, true).unwrap(), 2);
107///
108/// // Undirected path on 5 vertices — vc = 1 (the two endpoints
109/// // each have degree 1).
110/// let mut p = Graph::new(5, false).unwrap();
111/// for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4)] {
112///     p.add_edge(u, v).unwrap();
113/// }
114/// assert_eq!(vertex_connectivity(&p, true).unwrap(), 1);
115/// ```
116pub fn vertex_connectivity(graph: &Graph, checks: bool) -> IgraphResult<i64> {
117    let n = graph.vcount();
118
119    // Empty graph or single vertex: no pair to disconnect.
120    // Mirrors flow.c:2084-2087 (handled inside _connectivity_checks).
121    if n < 2 {
122        return Ok(0);
123    }
124
125    if checks {
126        // (1) Connectedness — disconnected ⇒ some pair separated ⇒ 0.
127        let connected = if graph.is_directed() {
128            strongly_connected_components(graph)?.count == 1
129        } else {
130            connected_components(graph)?.count == 1
131        };
132        if !connected {
133            return Ok(0);
134        }
135
136        // (2) Min-degree check (suggested by McMahan, flow.c:2069-2122).
137        // Undirected: any vertex with degree 1 ⇒ vc = 1.
138        // Directed: any vertex with out-degree 1 or in-degree 1 ⇒ vc = 1.
139        let min_one = if graph.is_directed() {
140            let mut hit = false;
141            for v in 0..n {
142                let out = graph.out_neighbors_vec(v)?.len();
143                let in_ = graph.in_neighbors_vec(v)?.len();
144                if out == 1 || in_ == 1 {
145                    hit = true;
146                    break;
147                }
148            }
149            hit
150        } else {
151            let mut hit = false;
152            for v in 0..n {
153                if graph.degree(v)? == 1 {
154                    hit = true;
155                    break;
156                }
157            }
158            hit
159        };
160        if min_one {
161            return Ok(1);
162        }
163
164        // (3) Complete graph ⇒ vc = n - 1. The C version splits this
165        // out from _connectivity_checks because that helper is reused
166        // for edge connectivity where the complete-graph short-circuit
167        // does not apply to multigraphs (flow.c:2168-2180).
168        if is_complete(graph)? {
169            return Ok(i64::from(n) - 1);
170        }
171    }
172
173    // Pairwise FL-013 loop.
174    //
175    // Initial min_conn = n - 1, matching the C upper bound at
176    // flow.c:1950. NumberOfNodes mode returns `n` for direct-edge
177    // pairs (vs C's `n - 1`); either way `n < n - 1` is false so
178    // direct-edge pairs never lower min_conn — same end result.
179    let mut min_conn = i64::from(n) - 1;
180    let directed = graph.is_directed();
181    for i in 0..n {
182        // Undirected: j > i (all pairs are unordered; vc(i,j) = vc(j,i)
183        // after the implicit IGRAPH_TO_DIRECTED_MUTUAL).
184        // Directed: j != i (vc(i,j) need not equal vc(j,i)).
185        let start = if directed { 0 } else { i + 1 };
186        for j in start..n {
187            if i == j {
188                continue;
189            }
190            let conn = st_vertex_connectivity(graph, i, j, VconnNei::NumberOfNodes)?;
191            if conn < min_conn {
192                min_conn = conn;
193                if min_conn == 0 {
194                    return Ok(0);
195                }
196            }
197        }
198    }
199
200    Ok(min_conn)
201}
202
203/// Group cohesion — igraph C alias `igraph_cohesion` (flow.c:2470).
204/// Exact synonym for [`vertex_connectivity`]; kept for naming parity
205/// with the upstream API and so users following the
206/// White-Harary (2001) sociological-network literature have a direct
207/// hit. Identical signature and behaviour.
208///
209/// # Examples
210///
211/// ```
212/// use rust_igraph::{Graph, cohesion};
213///
214/// // A cycle of 4 vertices: removing any single vertex still leaves a path.
215/// let mut g = Graph::with_vertices(4);
216/// g.add_edge(0, 1).unwrap();
217/// g.add_edge(1, 2).unwrap();
218/// g.add_edge(2, 3).unwrap();
219/// g.add_edge(3, 0).unwrap();
220/// assert_eq!(cohesion(&g, true).unwrap(), 2);
221/// ```
222pub fn cohesion(graph: &Graph, checks: bool) -> IgraphResult<i64> {
223    vertex_connectivity(graph, checks)
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    fn ring_graph_n(n: u32, directed: bool) -> Graph {
231        let mut g = Graph::new(n, directed).expect("graph");
232        for i in 0..n {
233            let j = (i + 1) % n;
234            g.add_edge(i, j).expect("edge");
235        }
236        g
237    }
238
239    fn path_graph_n(n: u32, directed: bool) -> Graph {
240        let mut g = Graph::new(n, directed).expect("graph");
241        for i in 0..(n - 1) {
242            g.add_edge(i, i + 1).expect("edge");
243        }
244        g
245    }
246
247    fn complete_undirected(n: u32) -> Graph {
248        let mut g = Graph::new(n, false).expect("graph");
249        for i in 0..n {
250            for j in (i + 1)..n {
251                g.add_edge(i, j).expect("edge");
252            }
253        }
254        g
255    }
256
257    fn complete_directed_mutual(n: u32) -> Graph {
258        let mut g = Graph::new(n, true).expect("graph");
259        for i in 0..n {
260            for j in 0..n {
261                if i != j {
262                    g.add_edge(i, j).expect("edge");
263                }
264            }
265        }
266        g
267    }
268
269    // --- C unit-test fixtures (igraph_cohesion.c) ---
270
271    #[test]
272    fn cohesion_c_fixture_directed_7v_equals_one() {
273        // edges: 0-1 0-2 1-2 1-3 2-4 3-4 3-5 4-5 1-6 6-3 5-0  (DIRECTED)
274        // Expected vc = 1.
275        let mut g = Graph::new(7, true).expect("graph");
276        for (u, v) in [
277            (0u32, 1u32),
278            (0, 2),
279            (1, 2),
280            (1, 3),
281            (2, 4),
282            (3, 4),
283            (3, 5),
284            (4, 5),
285            (1, 6),
286            (6, 3),
287            (5, 0),
288        ] {
289            g.add_edge(u, v).expect("edge");
290        }
291        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
292        assert_eq!(cohesion(&g, true).expect("vc"), 1);
293    }
294
295    #[test]
296    fn cohesion_c_fixture_undirected_7v_equals_two() {
297        // edges: 0-1 0-2 1-2 1-3 2-4 3-4 3-5 4-5 1-6 6-3  (UNDIRECTED)
298        // Expected vc = 2.
299        let mut g = Graph::new(7, false).expect("graph");
300        for (u, v) in [
301            (0u32, 1u32),
302            (0, 2),
303            (1, 2),
304            (1, 3),
305            (2, 4),
306            (3, 4),
307            (3, 5),
308            (4, 5),
309            (1, 6),
310            (6, 3),
311        ] {
312            g.add_edge(u, v).expect("edge");
313        }
314        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 2);
315        assert_eq!(cohesion(&g, true).expect("vc"), 2);
316    }
317
318    // --- Edge cases ---
319
320    #[test]
321    fn empty_graph_returns_zero() {
322        let g = Graph::new(0, false).expect("graph");
323        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
324        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 0);
325    }
326
327    #[test]
328    fn single_vertex_returns_zero() {
329        let g = Graph::new(1, false).expect("graph");
330        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
331    }
332
333    #[test]
334    fn two_disconnected_vertices_return_zero() {
335        let g = Graph::new(2, false).expect("graph");
336        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
337        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 0);
338    }
339
340    #[test]
341    fn k2_returns_one() {
342        // K_2 is complete: vc = n - 1 = 1.
343        let mut g = Graph::new(2, false).expect("graph");
344        g.add_edge(0, 1).expect("edge");
345        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
346        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
347    }
348
349    // --- R-igraph test parity (test-flow.R:131-138) ---
350
351    #[test]
352    fn path_5v_undirected_returns_one() {
353        let g = path_graph_n(5, false);
354        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
355        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
356    }
357
358    #[test]
359    fn two_isolated_edges_undirected_returns_zero() {
360        // make_graph(edges = c(1, 2, 3, 4)) — two disconnected edges
361        // 0-1 and 2-3.
362        let mut g = Graph::new(4, false).expect("graph");
363        g.add_edge(0, 1).expect("edge");
364        g.add_edge(2, 3).expect("edge");
365        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
366    }
367
368    #[test]
369    fn ring_5v_undirected_returns_two() {
370        let g = ring_graph_n(5, false);
371        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 2);
372        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 2);
373    }
374
375    // --- Complete-graph short-circuit ---
376
377    #[test]
378    fn complete_undirected_6v_returns_five() {
379        let g = complete_undirected(6);
380        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 5);
381        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 5);
382    }
383
384    #[test]
385    fn complete_directed_5v_returns_four() {
386        let g = complete_directed_mutual(5);
387        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 4);
388        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 4);
389    }
390
391    // --- py-igraph test parity (test_flow.py:27,29-30) ---
392
393    #[test]
394    fn out_tree_3ary_10v_returns_zero() {
395        // Graph.Tree(10, 3, "out") is a directed rooted out-tree (root
396        // → children only). Not strongly connected (leaves have no
397        // out-edges), so vc = 0.
398        let edges: &[(u32, u32)] = &[
399            (0, 1),
400            (0, 2),
401            (0, 3),
402            (1, 4),
403            (1, 5),
404            (1, 6),
405            (2, 7),
406            (2, 8),
407            (2, 9),
408        ];
409        let mut g = Graph::new(10, true).expect("graph");
410        for &(u, v) in edges {
411            g.add_edge(u, v).expect("edge");
412        }
413        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
414    }
415
416    #[test]
417    fn undirected_tree_3ary_10v_returns_one() {
418        // Same tree but undirected — connected, leaves have degree 1
419        // → cheap min-degree short-circuit gives 1.
420        let edges: &[(u32, u32)] = &[
421            (0, 1),
422            (0, 2),
423            (0, 3),
424            (1, 4),
425            (1, 5),
426            (1, 6),
427            (2, 7),
428            (2, 8),
429            (2, 9),
430        ];
431        let mut g = Graph::new(10, false).expect("graph");
432        for &(u, v) in edges {
433            g.add_edge(u, v).expect("edge");
434        }
435        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
436        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
437    }
438
439    // --- checks=false vs checks=true agreement ---
440
441    #[test]
442    fn checks_false_matches_checks_true_on_small_graphs() {
443        let fixtures: Vec<Graph> = vec![
444            ring_graph_n(6, false),
445            ring_graph_n(6, true),
446            path_graph_n(5, false),
447            complete_undirected(4),
448            complete_directed_mutual(4),
449        ];
450        for g in fixtures {
451            let with_checks = vertex_connectivity(&g, true).expect("vc");
452            let without = vertex_connectivity(&g, false).expect("vc");
453            assert_eq!(
454                with_checks,
455                without,
456                "checks={{true,false}} disagree on n={}, dir={}",
457                g.vcount(),
458                g.is_directed()
459            );
460        }
461    }
462
463    // --- Sanity: 2 internally vertex-disjoint paths between every pair ---
464
465    #[test]
466    fn two_disjoint_paths_giving_vc_two() {
467        // Wheel-like: 0-1-2-3-0 cycle plus chord 0-2 turning a triangle
468        // shape. Min degree = 2 (vertex 1 and 3); cycles ensure vc = 2.
469        let mut g = Graph::new(4, false).expect("graph");
470        for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 0), (0, 2)] {
471            g.add_edge(u, v).expect("edge");
472        }
473        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 2);
474    }
475}
476
477#[cfg(all(test, feature = "proptest-harness"))]
478mod proptests {
479    //! Proptest invariants:
480    //! * `vertex_connectivity` is bounded above by `n - 1`.
481    //! * `vertex_connectivity` is bounded above by the minimum degree
482    //!   (Whitney).
483    //! * Disconnected graphs return `0`.
484    //! * `checks=false` agrees with `checks=true`.
485
486    use super::*;
487    use crate::core::Graph;
488    use proptest::prelude::*;
489
490    fn xorshift(mut r: u64) -> u64 {
491        r ^= r << 13;
492        r ^= r >> 7;
493        r ^= r << 17;
494        r
495    }
496
497    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
498        let mut g = Graph::new(n, directed).expect("graph");
499        let mut state = seed | 1;
500        for _ in 0..m_max {
501            state = xorshift(state);
502            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
503            state = xorshift(state);
504            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
505            if u == v {
506                continue;
507            }
508            g.add_edge(u, v).expect("edge");
509        }
510        g
511    }
512
513    proptest! {
514        #[test]
515        fn vc_bounded_by_n_minus_one(
516            seed in any::<u64>(),
517            n in 2u32..7,
518            m in 0u32..14,
519            directed in any::<bool>(),
520        ) {
521            let g = build_random(seed, n, m, directed);
522            let vc = vertex_connectivity(&g, true).expect("vc");
523            prop_assert!(vc >= 0, "vc must be non-negative, got {vc}");
524            prop_assert!(vc <= i64::from(n) - 1,
525                "vc={vc} exceeds n-1={} (n={n})", i64::from(n) - 1);
526        }
527
528        #[test]
529        fn checks_true_matches_checks_false(
530            seed in any::<u64>(),
531            n in 2u32..6,
532            m in 0u32..12,
533            directed in any::<bool>(),
534        ) {
535            let g = build_random(seed, n, m, directed);
536            let with_checks = vertex_connectivity(&g, true).expect("vc");
537            let without = vertex_connectivity(&g, false).expect("vc");
538            prop_assert_eq!(with_checks, without,
539                "checks=true {} != checks=false {} (n={}, m={}, directed={}, seed={})",
540                with_checks, without, n, m, directed, seed);
541        }
542
543        #[test]
544        fn vc_bounded_by_min_degree_undirected(
545            seed in any::<u64>(),
546            n in 3u32..6,
547            m in 1u32..10,
548        ) {
549            // For undirected simple graphs, vc <= min degree (Whitney).
550            // Our random builder may produce parallel edges; vc still
551            // <= min-degree because each parallel edge contributes to
552            // degree but not to the connectivity beyond 1.
553            let g = build_random(seed, n, m, false);
554            let mut min_deg = u32::MAX;
555            for v in 0..n {
556                let d = u32::try_from(g.degree(v).expect("degree")).unwrap_or(u32::MAX);
557                if d < min_deg { min_deg = d; }
558            }
559            let vc = vertex_connectivity(&g, true).expect("vc");
560            // vc <= min_deg only meaningful when graph has no isolated
561            // vertices; when there are isolated vertices, vc = 0 = min_deg.
562            prop_assert!(vc <= i64::from(min_deg),
563                "vc={vc} > min_deg={min_deg} (n={n}, m={m}, seed={seed})");
564        }
565    }
566}