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.
208pub fn cohesion(graph: &Graph, checks: bool) -> IgraphResult<i64> {
209    vertex_connectivity(graph, checks)
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    fn ring_graph_n(n: u32, directed: bool) -> Graph {
217        let mut g = Graph::new(n, directed).expect("graph");
218        for i in 0..n {
219            let j = (i + 1) % n;
220            g.add_edge(i, j).expect("edge");
221        }
222        g
223    }
224
225    fn path_graph_n(n: u32, directed: bool) -> Graph {
226        let mut g = Graph::new(n, directed).expect("graph");
227        for i in 0..(n - 1) {
228            g.add_edge(i, i + 1).expect("edge");
229        }
230        g
231    }
232
233    fn complete_undirected(n: u32) -> Graph {
234        let mut g = Graph::new(n, false).expect("graph");
235        for i in 0..n {
236            for j in (i + 1)..n {
237                g.add_edge(i, j).expect("edge");
238            }
239        }
240        g
241    }
242
243    fn complete_directed_mutual(n: u32) -> Graph {
244        let mut g = Graph::new(n, true).expect("graph");
245        for i in 0..n {
246            for j in 0..n {
247                if i != j {
248                    g.add_edge(i, j).expect("edge");
249                }
250            }
251        }
252        g
253    }
254
255    // --- C unit-test fixtures (igraph_cohesion.c) ---
256
257    #[test]
258    fn cohesion_c_fixture_directed_7v_equals_one() {
259        // edges: 0-1 0-2 1-2 1-3 2-4 3-4 3-5 4-5 1-6 6-3 5-0  (DIRECTED)
260        // Expected vc = 1.
261        let mut g = Graph::new(7, true).expect("graph");
262        for (u, v) in [
263            (0u32, 1u32),
264            (0, 2),
265            (1, 2),
266            (1, 3),
267            (2, 4),
268            (3, 4),
269            (3, 5),
270            (4, 5),
271            (1, 6),
272            (6, 3),
273            (5, 0),
274        ] {
275            g.add_edge(u, v).expect("edge");
276        }
277        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
278        assert_eq!(cohesion(&g, true).expect("vc"), 1);
279    }
280
281    #[test]
282    fn cohesion_c_fixture_undirected_7v_equals_two() {
283        // edges: 0-1 0-2 1-2 1-3 2-4 3-4 3-5 4-5 1-6 6-3  (UNDIRECTED)
284        // Expected vc = 2.
285        let mut g = Graph::new(7, false).expect("graph");
286        for (u, v) in [
287            (0u32, 1u32),
288            (0, 2),
289            (1, 2),
290            (1, 3),
291            (2, 4),
292            (3, 4),
293            (3, 5),
294            (4, 5),
295            (1, 6),
296            (6, 3),
297        ] {
298            g.add_edge(u, v).expect("edge");
299        }
300        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 2);
301        assert_eq!(cohesion(&g, true).expect("vc"), 2);
302    }
303
304    // --- Edge cases ---
305
306    #[test]
307    fn empty_graph_returns_zero() {
308        let g = Graph::new(0, false).expect("graph");
309        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
310        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 0);
311    }
312
313    #[test]
314    fn single_vertex_returns_zero() {
315        let g = Graph::new(1, false).expect("graph");
316        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
317    }
318
319    #[test]
320    fn two_disconnected_vertices_return_zero() {
321        let g = Graph::new(2, false).expect("graph");
322        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
323        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 0);
324    }
325
326    #[test]
327    fn k2_returns_one() {
328        // K_2 is complete: vc = n - 1 = 1.
329        let mut g = Graph::new(2, false).expect("graph");
330        g.add_edge(0, 1).expect("edge");
331        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
332        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
333    }
334
335    // --- R-igraph test parity (test-flow.R:131-138) ---
336
337    #[test]
338    fn path_5v_undirected_returns_one() {
339        let g = path_graph_n(5, false);
340        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
341        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
342    }
343
344    #[test]
345    fn two_isolated_edges_undirected_returns_zero() {
346        // make_graph(edges = c(1, 2, 3, 4)) — two disconnected edges
347        // 0-1 and 2-3.
348        let mut g = Graph::new(4, false).expect("graph");
349        g.add_edge(0, 1).expect("edge");
350        g.add_edge(2, 3).expect("edge");
351        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
352    }
353
354    #[test]
355    fn ring_5v_undirected_returns_two() {
356        let g = ring_graph_n(5, false);
357        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 2);
358        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 2);
359    }
360
361    // --- Complete-graph short-circuit ---
362
363    #[test]
364    fn complete_undirected_6v_returns_five() {
365        let g = complete_undirected(6);
366        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 5);
367        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 5);
368    }
369
370    #[test]
371    fn complete_directed_5v_returns_four() {
372        let g = complete_directed_mutual(5);
373        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 4);
374        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 4);
375    }
376
377    // --- py-igraph test parity (test_flow.py:27,29-30) ---
378
379    #[test]
380    fn out_tree_3ary_10v_returns_zero() {
381        // Graph.Tree(10, 3, "out") is a directed rooted out-tree (root
382        // → children only). Not strongly connected (leaves have no
383        // out-edges), so vc = 0.
384        let edges: &[(u32, u32)] = &[
385            (0, 1),
386            (0, 2),
387            (0, 3),
388            (1, 4),
389            (1, 5),
390            (1, 6),
391            (2, 7),
392            (2, 8),
393            (2, 9),
394        ];
395        let mut g = Graph::new(10, true).expect("graph");
396        for &(u, v) in edges {
397            g.add_edge(u, v).expect("edge");
398        }
399        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 0);
400    }
401
402    #[test]
403    fn undirected_tree_3ary_10v_returns_one() {
404        // Same tree but undirected — connected, leaves have degree 1
405        // → cheap min-degree short-circuit gives 1.
406        let edges: &[(u32, u32)] = &[
407            (0, 1),
408            (0, 2),
409            (0, 3),
410            (1, 4),
411            (1, 5),
412            (1, 6),
413            (2, 7),
414            (2, 8),
415            (2, 9),
416        ];
417        let mut g = Graph::new(10, false).expect("graph");
418        for &(u, v) in edges {
419            g.add_edge(u, v).expect("edge");
420        }
421        assert_eq!(vertex_connectivity(&g, true).expect("vc"), 1);
422        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 1);
423    }
424
425    // --- checks=false vs checks=true agreement ---
426
427    #[test]
428    fn checks_false_matches_checks_true_on_small_graphs() {
429        let fixtures: Vec<Graph> = vec![
430            ring_graph_n(6, false),
431            ring_graph_n(6, true),
432            path_graph_n(5, false),
433            complete_undirected(4),
434            complete_directed_mutual(4),
435        ];
436        for g in fixtures {
437            let with_checks = vertex_connectivity(&g, true).expect("vc");
438            let without = vertex_connectivity(&g, false).expect("vc");
439            assert_eq!(
440                with_checks,
441                without,
442                "checks={{true,false}} disagree on n={}, dir={}",
443                g.vcount(),
444                g.is_directed()
445            );
446        }
447    }
448
449    // --- Sanity: 2 internally vertex-disjoint paths between every pair ---
450
451    #[test]
452    fn two_disjoint_paths_giving_vc_two() {
453        // Wheel-like: 0-1-2-3-0 cycle plus chord 0-2 turning a triangle
454        // shape. Min degree = 2 (vertex 1 and 3); cycles ensure vc = 2.
455        let mut g = Graph::new(4, false).expect("graph");
456        for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 0), (0, 2)] {
457            g.add_edge(u, v).expect("edge");
458        }
459        assert_eq!(vertex_connectivity(&g, false).expect("vc"), 2);
460    }
461}
462
463#[cfg(all(test, feature = "proptest-harness"))]
464mod proptests {
465    //! Proptest invariants:
466    //! * `vertex_connectivity` is bounded above by `n - 1`.
467    //! * `vertex_connectivity` is bounded above by the minimum degree
468    //!   (Whitney).
469    //! * Disconnected graphs return `0`.
470    //! * `checks=false` agrees with `checks=true`.
471
472    use super::*;
473    use crate::core::Graph;
474    use proptest::prelude::*;
475
476    fn xorshift(mut r: u64) -> u64 {
477        r ^= r << 13;
478        r ^= r >> 7;
479        r ^= r << 17;
480        r
481    }
482
483    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
484        let mut g = Graph::new(n, directed).expect("graph");
485        let mut state = seed | 1;
486        for _ in 0..m_max {
487            state = xorshift(state);
488            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
489            state = xorshift(state);
490            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
491            if u == v {
492                continue;
493            }
494            g.add_edge(u, v).expect("edge");
495        }
496        g
497    }
498
499    proptest! {
500        #[test]
501        fn vc_bounded_by_n_minus_one(
502            seed in any::<u64>(),
503            n in 2u32..7,
504            m in 0u32..14,
505            directed in any::<bool>(),
506        ) {
507            let g = build_random(seed, n, m, directed);
508            let vc = vertex_connectivity(&g, true).expect("vc");
509            prop_assert!(vc >= 0, "vc must be non-negative, got {vc}");
510            prop_assert!(vc <= i64::from(n) - 1,
511                "vc={vc} exceeds n-1={} (n={n})", i64::from(n) - 1);
512        }
513
514        #[test]
515        fn checks_true_matches_checks_false(
516            seed in any::<u64>(),
517            n in 2u32..6,
518            m in 0u32..12,
519            directed in any::<bool>(),
520        ) {
521            let g = build_random(seed, n, m, directed);
522            let with_checks = vertex_connectivity(&g, true).expect("vc");
523            let without = vertex_connectivity(&g, false).expect("vc");
524            prop_assert_eq!(with_checks, without,
525                "checks=true {} != checks=false {} (n={}, m={}, directed={}, seed={})",
526                with_checks, without, n, m, directed, seed);
527        }
528
529        #[test]
530        fn vc_bounded_by_min_degree_undirected(
531            seed in any::<u64>(),
532            n in 3u32..6,
533            m in 1u32..10,
534        ) {
535            // For undirected simple graphs, vc <= min degree (Whitney).
536            // Our random builder may produce parallel edges; vc still
537            // <= min-degree because each parallel edge contributes to
538            // degree but not to the connectivity beyond 1.
539            let g = build_random(seed, n, m, false);
540            let mut min_deg = u32::MAX;
541            for v in 0..n {
542                let d = u32::try_from(g.degree(v).expect("degree")).unwrap_or(u32::MAX);
543                if d < min_deg { min_deg = d; }
544            }
545            let vc = vertex_connectivity(&g, true).expect("vc");
546            // vc <= min_deg only meaningful when graph has no isolated
547            // vertices; when there are isolated vertices, vc = 0 = min_deg.
548            prop_assert!(vc <= i64::from(min_deg),
549                "vc={vc} > min_deg={min_deg} (n={n}, m={m}, seed={seed})");
550        }
551    }
552}