Skip to main content

rust_igraph/algorithms/flow/
edge_connectivity.rs

1//! `edge_connectivity` (ALGO-FL-016) — global graph adhesion: the
2//! minimum number of edges whose removal disconnects some pair of
3//! vertices in the graph.
4//!
5//! Counterpart of `igraph_edge_connectivity` in
6//! `references/igraph/src/flow/flow.c:2270`. Equivalent to the C
7//! alias `igraph_adhesion` (flow.c:2433) and to python-igraph's
8//! `Graph.edge_connectivity()` (and `Graph.adhesion()`),
9//! R-igraph's `edge_connectivity()` (no source/target).
10//!
11//! ## Algorithm
12//!
13//! Definition: `lambda(G) = min_{s ≠ t} st_edge_connectivity(s, t)`.
14//!
15//! Optional cheap short-circuits when `checks = true` (suggested by
16//! Peter `McMahan` per the upstream C docstring, see
17//! flow.c:2253-2259) — shared with [`super::vertex_connectivity`] via
18//! the same `_connectivity_checks` helper inlined here:
19//!
20//! 1. Empty/singleton graph → `0`.
21//! 2. Disconnected (weakly for undirected, strongly for directed)
22//!    → `0`.
23//! 3. Any vertex with `min(in, out) = 1` (or undirected degree
24//!    `= 1`) → `1` (its incident edge is a bridge for that vertex).
25//!
26//! Crucially, we do **not** short-circuit on complete graphs. The
27//! upstream C comment (flow.c:2168-2180) calls this out: completeness
28//! alone does not determine the edge connectivity of a multigraph,
29//! because parallel edges raise edge connectivity beyond `n - 1`.
30//!
31//! Otherwise we run the same fixed-vertex iteration as
32//! `igraph_mincut_value` (flow.c:1706-1723): pick vertex `0`, and for
33//! every other vertex `v` compute `st_edge_connectivity(0, v)`
34//! (undirected) or both `(0, v)` and `(v, 0)` (directed). The minimum
35//! over all such pairs equals the global edge connectivity, because
36//! every global min-cut separates `0` from at least one vertex on the
37//! other side.
38//!
39//! ## Complexity
40//!
41//! `O(V)` calls to FL-011 for undirected, `O(2V)` for directed. Each
42//! FL-011 inherits FL-002 = `O(V·E²)` on unit caps, so the total is
43//! `O(V²·E²)` worst case. The igraph C docstring reports
44//! `O(log V · V²)` for undirected (their Stoer-Wagner implementation,
45//! not ported here) and `O(V⁴)` for directed.
46//!
47//! ## Why fixed-vertex iteration is correct
48//!
49//! For any global min-cut `(S, V \ S)` containing vertex `0`, there
50//! exists some `v ∈ V \ S` with `v ≠ 0`. The cut is then a feasible
51//! `0 → v` cut (undirected) or `0 → v` / `v → 0` cut (directed), so
52//! `st_edge_connectivity(0, v) ≤ |cut| = lambda(G)`. Combined with
53//! `lambda(G) ≤ st_edge_connectivity(0, v)` for every `v` (any
54//! `0-v` min-cut is a feasible global cut), we get equality.
55
56use crate::core::{Graph, IgraphResult};
57
58use super::st_edge_connectivity::st_edge_connectivity;
59use crate::algorithms::connectivity::components::connected_components;
60use crate::algorithms::connectivity::strong::strongly_connected_components;
61
62/// Edge connectivity (adhesion) of a graph.
63///
64/// Returns the minimum number of edges that must be removed to
65/// disconnect *some* pair of vertices in `graph`. Equal to
66/// `min_{s ≠ t} st_edge_connectivity(s, t)`.
67///
68/// Counterpart of `igraph_edge_connectivity`
69/// (`references/igraph/src/flow/flow.c:2270`) and its alias
70/// `igraph_adhesion` (flow.c:2433).
71///
72/// # Arguments
73///
74/// * `graph` — input graph (directed or undirected).
75/// * `checks` — when `true`, run the cheap short-circuits described
76///   in the module docs before falling back to the fixed-vertex
77///   `O(V)`-flows loop. Recommended for any non-trivial graph; the
78///   helpers cost `O(V + E)` and can replace the whole loop.
79///   Equivalent to igraph C's `checks` argument.
80///
81/// # Returns
82///
83/// The edge connectivity as `i64`. Returns:
84/// * `0` when `vcount() ≤ 1`, or the graph is disconnected.
85/// * `1` when there is a vertex with degree `1` (undirected) or
86///   `min(in, out) = 1` (directed).
87/// * Otherwise, the result of the fixed-vertex `st_edge_connectivity`
88///   loop from vertex `0`.
89///
90/// # Errors
91///
92/// Propagates errors from [`st_edge_connectivity`],
93/// [`connected_components`], and [`strongly_connected_components`].
94/// In practice these arise only from arithmetic overflow on very
95/// large graphs, which is unreachable here.
96///
97/// [`IgraphError`]: crate::core::IgraphError
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, edge_connectivity};
103///
104/// // Undirected ring on 5 vertices — lambda = 2 (any two non-adjacent
105/// // edges form a min-cut).
106/// let mut g = Graph::new(5, false).unwrap();
107/// for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)] {
108///     g.add_edge(u, v).unwrap();
109/// }
110/// assert_eq!(edge_connectivity(&g, true).unwrap(), 2);
111///
112/// // Undirected path on 5 vertices — lambda = 1 (any edge is a
113/// // bridge).
114/// let mut p = Graph::new(5, false).unwrap();
115/// for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4)] {
116///     p.add_edge(u, v).unwrap();
117/// }
118/// assert_eq!(edge_connectivity(&p, true).unwrap(), 1);
119/// ```
120pub fn edge_connectivity(graph: &Graph, checks: bool) -> IgraphResult<i64> {
121    let n = graph.vcount();
122
123    // Mirrors flow.c:2281-2284 — singleton/empty graph short-circuit
124    // (catches the `igraph_mincut_value = +inf` corner case for n=1
125    // up front).
126    if n <= 1 {
127        return Ok(0);
128    }
129
130    if checks {
131        // (1) Connectedness — disconnected ⇒ some pair separated ⇒ 0.
132        let connected = if graph.is_directed() {
133            strongly_connected_components(graph)?.count == 1
134        } else {
135            connected_components(graph)?.count == 1
136        };
137        if !connected {
138            return Ok(0);
139        }
140
141        // (2) Min-degree check (suggested by McMahan, flow.c:2069-2122).
142        // Undirected: any vertex with degree 1 ⇒ lambda = 1.
143        // Directed: any vertex with out-degree 1 or in-degree 1
144        // ⇒ lambda = 1. (Whitney: lambda ≤ min-degree.)
145        let min_one = if graph.is_directed() {
146            let mut hit = false;
147            for v in 0..n {
148                let out = graph.out_neighbors_vec(v)?.len();
149                let in_ = graph.in_neighbors_vec(v)?.len();
150                if out == 1 || in_ == 1 {
151                    hit = true;
152                    break;
153                }
154            }
155            hit
156        } else {
157            let mut hit = false;
158            for v in 0..n {
159                if graph.degree(v)? == 1 {
160                    hit = true;
161                    break;
162                }
163            }
164            hit
165        };
166        if min_one {
167            return Ok(1);
168        }
169        // Note: deliberately no complete-graph short-circuit (see
170        // flow.c:2168-2180 — completeness alone does not determine
171        // the edge connectivity of a multigraph).
172    }
173
174    // Fixed-vertex iteration — mirrors `igraph_mincut_value`
175    // (flow.c:1706-1723). lambda(G) = min over v != 0 of
176    // st_edge_connectivity(0, v) [+ symmetric direction if directed].
177    let mut min_lambda = i64::MAX;
178    let directed = graph.is_directed();
179    for v in 1..n {
180        let f = st_edge_connectivity(graph, 0, v)?;
181        if f < min_lambda {
182            min_lambda = f;
183            if min_lambda == 0 {
184                return Ok(0);
185            }
186        }
187        if directed {
188            let f2 = st_edge_connectivity(graph, v, 0)?;
189            if f2 < min_lambda {
190                min_lambda = f2;
191                if min_lambda == 0 {
192                    return Ok(0);
193                }
194            }
195        }
196    }
197
198    Ok(min_lambda)
199}
200
201/// Group adhesion — igraph C alias `igraph_adhesion` (flow.c:2433).
202/// Exact synonym for [`edge_connectivity`]; kept for naming parity
203/// with the upstream API and so users following the
204/// White-Harary (2001) sociological-network literature have a direct
205/// hit. Identical signature and behaviour.
206pub fn adhesion(graph: &Graph, checks: bool) -> IgraphResult<i64> {
207    edge_connectivity(graph, checks)
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    fn ring_graph_n(n: u32, directed: bool) -> Graph {
215        let mut g = Graph::new(n, directed).expect("graph");
216        for i in 0..n {
217            let j = (i + 1) % n;
218            g.add_edge(i, j).expect("edge");
219        }
220        g
221    }
222
223    fn path_graph_n(n: u32, directed: bool) -> Graph {
224        let mut g = Graph::new(n, directed).expect("graph");
225        for i in 0..(n - 1) {
226            g.add_edge(i, i + 1).expect("edge");
227        }
228        g
229    }
230
231    fn complete_undirected(n: u32) -> Graph {
232        let mut g = Graph::new(n, false).expect("graph");
233        for i in 0..n {
234            for j in (i + 1)..n {
235                g.add_edge(i, j).expect("edge");
236            }
237        }
238        g
239    }
240
241    fn complete_directed_mutual(n: u32) -> Graph {
242        let mut g = Graph::new(n, true).expect("graph");
243        for i in 0..n {
244            for j in 0..n {
245                if i != j {
246                    g.add_edge(i, j).expect("edge");
247                }
248            }
249        }
250        g
251    }
252
253    // --- Edge cases ---
254
255    #[test]
256    fn empty_graph_returns_zero() {
257        let g = Graph::new(0, false).expect("graph");
258        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
259        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
260    }
261
262    #[test]
263    fn single_vertex_returns_zero() {
264        let g = Graph::new(1, false).expect("graph");
265        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
266        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
267    }
268
269    #[test]
270    fn two_disconnected_vertices_return_zero() {
271        let g = Graph::new(2, false).expect("graph");
272        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
273        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
274    }
275
276    #[test]
277    fn k2_undirected_returns_one() {
278        // K_2 single edge — lambda = 1.
279        let mut g = Graph::new(2, false).expect("graph");
280        g.add_edge(0, 1).expect("edge");
281        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
282        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
283    }
284
285    // --- R-igraph test parity (test-flow.R) ---
286
287    #[test]
288    fn path_5v_undirected_returns_one() {
289        let g = path_graph_n(5, false);
290        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
291        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
292        assert_eq!(adhesion(&g, true).expect("ec"), 1);
293    }
294
295    #[test]
296    fn two_isolated_edges_undirected_returns_zero() {
297        let mut g = Graph::new(4, false).expect("graph");
298        g.add_edge(0, 1).expect("edge");
299        g.add_edge(2, 3).expect("edge");
300        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
301        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
302    }
303
304    #[test]
305    fn ring_5v_undirected_returns_two() {
306        let g = ring_graph_n(5, false);
307        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
308        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
309        assert_eq!(adhesion(&g, true).expect("ec"), 2);
310    }
311
312    // --- Complete-graph: no cheap short-circuit, but the fixed-vertex
313    //     loop still returns n - 1 for simple K_n. ---
314
315    #[test]
316    fn complete_undirected_5v_returns_four() {
317        let g = complete_undirected(5);
318        // lambda(K_5) = 4. checks=true short-circuits at min-degree=4
319        // only after passing through; here we hit the full loop path
320        // because min-degree=4 != 1.
321        assert_eq!(edge_connectivity(&g, true).expect("ec"), 4);
322        assert_eq!(edge_connectivity(&g, false).expect("ec"), 4);
323    }
324
325    #[test]
326    fn complete_directed_mutual_4v_returns_three() {
327        let g = complete_directed_mutual(4);
328        // For mutual K_4: every pair has 3 directed paths, so
329        // lambda = 3.
330        assert_eq!(edge_connectivity(&g, true).expect("ec"), 3);
331        assert_eq!(edge_connectivity(&g, false).expect("ec"), 3);
332    }
333
334    // --- Multigraph fixture: completeness alone does not bound lambda. ---
335
336    #[test]
337    fn multigraph_two_parallel_edges_doubles_lambda() {
338        // 2 vertices, 2 parallel undirected edges — lambda = 2 even
339        // though it is "complete" on 2 vertices.
340        let mut g = Graph::new(2, false).expect("graph");
341        g.add_edge(0, 1).expect("edge");
342        g.add_edge(0, 1).expect("edge");
343        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
344        // With checks=true the min-degree=2 check does NOT short-circuit
345        // (only min-degree=1 does), so the fixed-vertex loop runs.
346        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
347    }
348
349    // --- py-igraph test parity ---
350
351    #[test]
352    fn out_tree_3ary_10v_returns_zero() {
353        // Graph.Tree(10, 3, "out") — not strongly connected (leaves
354        // have no out-edges), so lambda = 0.
355        let edges: &[(u32, u32)] = &[
356            (0, 1),
357            (0, 2),
358            (0, 3),
359            (1, 4),
360            (1, 5),
361            (1, 6),
362            (2, 7),
363            (2, 8),
364            (2, 9),
365        ];
366        let mut g = Graph::new(10, true).expect("graph");
367        for &(u, v) in edges {
368            g.add_edge(u, v).expect("edge");
369        }
370        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
371        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
372    }
373
374    #[test]
375    fn undirected_tree_3ary_10v_returns_one() {
376        // Same tree but undirected — connected, every edge is a
377        // bridge → lambda = 1.
378        let edges: &[(u32, u32)] = &[
379            (0, 1),
380            (0, 2),
381            (0, 3),
382            (1, 4),
383            (1, 5),
384            (1, 6),
385            (2, 7),
386            (2, 8),
387            (2, 9),
388        ];
389        let mut g = Graph::new(10, false).expect("graph");
390        for &(u, v) in edges {
391            g.add_edge(u, v).expect("edge");
392        }
393        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
394        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
395    }
396
397    // --- Directed cycle: lambda = 1 (every arc is a directed bridge). ---
398
399    #[test]
400    fn directed_cycle_6v_returns_one() {
401        let g = ring_graph_n(6, true);
402        // Strongly connected (it's a directed cycle), min in/out = 1
403        // ⇒ cheap short-circuit returns 1. Without checks, the
404        // fixed-vertex loop still finds st_edge_conn(0, v) = 1.
405        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
406        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
407    }
408
409    // --- 4-cycle with chord: cheap short-circuit fails (min-deg=2,
410    //     not complete in the FL-015 sense), full loop returns 2. ---
411
412    #[test]
413    fn cycle_with_chord_undirected_returns_two() {
414        let mut g = Graph::new(4, false).expect("graph");
415        for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 0), (0, 2)] {
416            g.add_edge(u, v).expect("edge");
417        }
418        // Vertices 1 and 3 have degree 2 — lambda = 2.
419        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
420        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
421    }
422
423    // --- C unit-test fixture parity (igraph_edge_connectivity.c style) ---
424
425    #[test]
426    fn c_fixture_directed_6v_equals_one() {
427        // 6 vertices, 8 directed arcs (mirrors st_edge_connectivity
428        // C fixture). Without a 5→0 back-arc this graph is not
429        // strongly connected — lambda = 0.
430        let mut g = Graph::new(6, true).expect("graph");
431        let arcs = [
432            (0u32, 1u32),
433            (0, 2),
434            (1, 2),
435            (1, 3),
436            (2, 4),
437            (3, 4),
438            (3, 5),
439            (4, 5),
440        ];
441        for (u, v) in arcs {
442            g.add_edge(u, v).expect("edge");
443        }
444        // Source 0 has no incoming, sink 5 has no outgoing —
445        // strongly disconnected.
446        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
447        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
448    }
449
450    #[test]
451    fn c_fixture_undirected_6v_equals_two() {
452        let mut g = Graph::new(6, false).expect("graph");
453        let arcs = [
454            (0u32, 1u32),
455            (0, 2),
456            (1, 2),
457            (1, 3),
458            (2, 4),
459            (3, 4),
460            (3, 5),
461            (4, 5),
462        ];
463        for (u, v) in arcs {
464            g.add_edge(u, v).expect("edge");
465        }
466        // All vertices have degree ≥ 2; lambda = 2 (e.g. cut {3-5, 4-5}
467        // isolates vertex 5).
468        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
469        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
470    }
471
472    // --- checks=false vs checks=true agreement ---
473
474    #[test]
475    fn checks_false_matches_checks_true_on_small_graphs() {
476        let fixtures: Vec<Graph> = vec![
477            ring_graph_n(6, false),
478            ring_graph_n(6, true),
479            path_graph_n(5, false),
480            complete_undirected(4),
481            complete_directed_mutual(4),
482        ];
483        for g in fixtures {
484            let with_checks = edge_connectivity(&g, true).expect("ec");
485            let without = edge_connectivity(&g, false).expect("ec");
486            assert_eq!(
487                with_checks,
488                without,
489                "checks={{true,false}} disagree on n={}, dir={}",
490                g.vcount(),
491                g.is_directed()
492            );
493        }
494    }
495
496    // --- adhesion alias parity ---
497
498    #[test]
499    fn adhesion_alias_matches_edge_connectivity() {
500        let fixtures: Vec<Graph> = vec![
501            ring_graph_n(7, false),
502            complete_undirected(5),
503            path_graph_n(4, false),
504        ];
505        for g in fixtures {
506            assert_eq!(
507                edge_connectivity(&g, true).expect("ec"),
508                adhesion(&g, true).expect("ec"),
509                "adhesion/edge_connectivity mismatch on n={}",
510                g.vcount(),
511            );
512        }
513    }
514}
515
516#[cfg(all(test, feature = "proptest-harness"))]
517mod proptests {
518    //! Proptest invariants:
519    //! * `0 <= edge_connectivity <= min_degree` (Whitney 1932).
520    //! * `edge_connectivity <= st_edge_connectivity(s, t)` for every
521    //!   pair `(s, t)`.
522    //! * `checks=true` agrees with `checks=false`.
523    //! * On disconnected graphs (cheap to detect), the result is `0`.
524
525    use super::*;
526    use crate::core::Graph;
527    use proptest::prelude::*;
528
529    fn xorshift(mut r: u64) -> u64 {
530        r ^= r << 13;
531        r ^= r >> 7;
532        r ^= r << 17;
533        r
534    }
535
536    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
537        let mut g = Graph::new(n, directed).expect("graph");
538        let mut state = seed | 1;
539        for _ in 0..m_max {
540            state = xorshift(state);
541            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
542            state = xorshift(state);
543            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
544            if u == v {
545                continue;
546            }
547            g.add_edge(u, v).expect("edge");
548        }
549        g
550    }
551
552    proptest! {
553        #[test]
554        fn ec_nonnegative_and_bounded_by_n_minus_one(
555            seed in any::<u64>(),
556            n in 2u32..7,
557            m in 0u32..14,
558            directed in any::<bool>(),
559        ) {
560            let g = build_random(seed, n, m, directed);
561            let ec = edge_connectivity(&g, true).expect("ec");
562            prop_assert!(ec >= 0, "ec must be non-negative, got {ec}");
563            // ec is unbounded above by n - 1 for multigraphs, but is
564            // bounded by m. (Multigraphs from our random builder can
565            // produce parallel edges.)
566            prop_assert!(ec as u64 <= u64::from(m),
567                "ec={ec} > m={m} (n={n}, seed={seed})");
568        }
569
570        #[test]
571        fn checks_true_matches_checks_false(
572            seed in any::<u64>(),
573            n in 2u32..6,
574            m in 0u32..12,
575            directed in any::<bool>(),
576        ) {
577            let g = build_random(seed, n, m, directed);
578            let with_checks = edge_connectivity(&g, true).expect("ec");
579            let without = edge_connectivity(&g, false).expect("ec");
580            prop_assert_eq!(with_checks, without,
581                "checks=true {} != checks=false {} (n={}, m={}, directed={}, seed={})",
582                with_checks, without, n, m, directed, seed);
583        }
584
585        #[test]
586        fn ec_bounded_by_min_degree_undirected(
587            seed in any::<u64>(),
588            n in 3u32..6,
589            m in 1u32..10,
590        ) {
591            // Whitney: lambda(G) <= min-degree(G).
592            let g = build_random(seed, n, m, false);
593            let mut min_deg = u32::MAX;
594            for v in 0..n {
595                let d = u32::try_from(g.degree(v).expect("degree")).unwrap_or(u32::MAX);
596                if d < min_deg { min_deg = d; }
597            }
598            let ec = edge_connectivity(&g, true).expect("ec");
599            prop_assert!(ec <= i64::from(min_deg),
600                "ec={ec} > min_deg={min_deg} (n={n}, m={m}, seed={seed})");
601        }
602
603        #[test]
604        fn ec_bounded_by_any_pair_st_edge_connectivity(
605            seed in any::<u64>(),
606            n in 3u32..5,
607            m in 1u32..9,
608            directed in any::<bool>(),
609        ) {
610            // lambda(G) <= st_edge_connectivity(s, t) for any (s, t).
611            use super::super::st_edge_connectivity::st_edge_connectivity;
612            let g = build_random(seed, n, m, directed);
613            let ec = edge_connectivity(&g, true).expect("ec");
614            for s in 0..n {
615                for t in 0..n {
616                    if s == t { continue; }
617                    let st = st_edge_connectivity(&g, s, t).expect("st_ec");
618                    prop_assert!(ec <= st,
619                        "ec={ec} > st_ec({s},{t})={st} (n={n}, m={m}, dir={directed}, seed={seed})");
620                }
621            }
622        }
623    }
624}