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.
206///
207/// # Examples
208///
209/// ```
210/// use rust_igraph::{Graph, adhesion};
211///
212/// let mut g = Graph::with_vertices(4);
213/// g.add_edge(0, 1).unwrap();
214/// g.add_edge(1, 2).unwrap();
215/// g.add_edge(2, 3).unwrap();
216/// g.add_edge(3, 0).unwrap();
217/// assert_eq!(adhesion(&g, true).unwrap(), 2);
218/// ```
219pub fn adhesion(graph: &Graph, checks: bool) -> IgraphResult<i64> {
220    edge_connectivity(graph, checks)
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn ring_graph_n(n: u32, directed: bool) -> Graph {
228        let mut g = Graph::new(n, directed).expect("graph");
229        for i in 0..n {
230            let j = (i + 1) % n;
231            g.add_edge(i, j).expect("edge");
232        }
233        g
234    }
235
236    fn path_graph_n(n: u32, directed: bool) -> Graph {
237        let mut g = Graph::new(n, directed).expect("graph");
238        for i in 0..(n - 1) {
239            g.add_edge(i, i + 1).expect("edge");
240        }
241        g
242    }
243
244    fn complete_undirected(n: u32) -> Graph {
245        let mut g = Graph::new(n, false).expect("graph");
246        for i in 0..n {
247            for j in (i + 1)..n {
248                g.add_edge(i, j).expect("edge");
249            }
250        }
251        g
252    }
253
254    fn complete_directed_mutual(n: u32) -> Graph {
255        let mut g = Graph::new(n, true).expect("graph");
256        for i in 0..n {
257            for j in 0..n {
258                if i != j {
259                    g.add_edge(i, j).expect("edge");
260                }
261            }
262        }
263        g
264    }
265
266    // --- Edge cases ---
267
268    #[test]
269    fn empty_graph_returns_zero() {
270        let g = Graph::new(0, false).expect("graph");
271        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
272        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
273    }
274
275    #[test]
276    fn single_vertex_returns_zero() {
277        let g = Graph::new(1, false).expect("graph");
278        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
279        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
280    }
281
282    #[test]
283    fn two_disconnected_vertices_return_zero() {
284        let g = Graph::new(2, false).expect("graph");
285        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
286        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
287    }
288
289    #[test]
290    fn k2_undirected_returns_one() {
291        // K_2 single edge — lambda = 1.
292        let mut g = Graph::new(2, false).expect("graph");
293        g.add_edge(0, 1).expect("edge");
294        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
295        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
296    }
297
298    // --- R-igraph test parity (test-flow.R) ---
299
300    #[test]
301    fn path_5v_undirected_returns_one() {
302        let g = path_graph_n(5, false);
303        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
304        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
305        assert_eq!(adhesion(&g, true).expect("ec"), 1);
306    }
307
308    #[test]
309    fn two_isolated_edges_undirected_returns_zero() {
310        let mut g = Graph::new(4, false).expect("graph");
311        g.add_edge(0, 1).expect("edge");
312        g.add_edge(2, 3).expect("edge");
313        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
314        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
315    }
316
317    #[test]
318    fn ring_5v_undirected_returns_two() {
319        let g = ring_graph_n(5, false);
320        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
321        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
322        assert_eq!(adhesion(&g, true).expect("ec"), 2);
323    }
324
325    // --- Complete-graph: no cheap short-circuit, but the fixed-vertex
326    //     loop still returns n - 1 for simple K_n. ---
327
328    #[test]
329    fn complete_undirected_5v_returns_four() {
330        let g = complete_undirected(5);
331        // lambda(K_5) = 4. checks=true short-circuits at min-degree=4
332        // only after passing through; here we hit the full loop path
333        // because min-degree=4 != 1.
334        assert_eq!(edge_connectivity(&g, true).expect("ec"), 4);
335        assert_eq!(edge_connectivity(&g, false).expect("ec"), 4);
336    }
337
338    #[test]
339    fn complete_directed_mutual_4v_returns_three() {
340        let g = complete_directed_mutual(4);
341        // For mutual K_4: every pair has 3 directed paths, so
342        // lambda = 3.
343        assert_eq!(edge_connectivity(&g, true).expect("ec"), 3);
344        assert_eq!(edge_connectivity(&g, false).expect("ec"), 3);
345    }
346
347    // --- Multigraph fixture: completeness alone does not bound lambda. ---
348
349    #[test]
350    fn multigraph_two_parallel_edges_doubles_lambda() {
351        // 2 vertices, 2 parallel undirected edges — lambda = 2 even
352        // though it is "complete" on 2 vertices.
353        let mut g = Graph::new(2, false).expect("graph");
354        g.add_edge(0, 1).expect("edge");
355        g.add_edge(0, 1).expect("edge");
356        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
357        // With checks=true the min-degree=2 check does NOT short-circuit
358        // (only min-degree=1 does), so the fixed-vertex loop runs.
359        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
360    }
361
362    // --- py-igraph test parity ---
363
364    #[test]
365    fn out_tree_3ary_10v_returns_zero() {
366        // Graph.Tree(10, 3, "out") — not strongly connected (leaves
367        // have no out-edges), so lambda = 0.
368        let edges: &[(u32, u32)] = &[
369            (0, 1),
370            (0, 2),
371            (0, 3),
372            (1, 4),
373            (1, 5),
374            (1, 6),
375            (2, 7),
376            (2, 8),
377            (2, 9),
378        ];
379        let mut g = Graph::new(10, true).expect("graph");
380        for &(u, v) in edges {
381            g.add_edge(u, v).expect("edge");
382        }
383        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
384        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
385    }
386
387    #[test]
388    fn undirected_tree_3ary_10v_returns_one() {
389        // Same tree but undirected — connected, every edge is a
390        // bridge → lambda = 1.
391        let edges: &[(u32, u32)] = &[
392            (0, 1),
393            (0, 2),
394            (0, 3),
395            (1, 4),
396            (1, 5),
397            (1, 6),
398            (2, 7),
399            (2, 8),
400            (2, 9),
401        ];
402        let mut g = Graph::new(10, false).expect("graph");
403        for &(u, v) in edges {
404            g.add_edge(u, v).expect("edge");
405        }
406        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
407        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
408    }
409
410    // --- Directed cycle: lambda = 1 (every arc is a directed bridge). ---
411
412    #[test]
413    fn directed_cycle_6v_returns_one() {
414        let g = ring_graph_n(6, true);
415        // Strongly connected (it's a directed cycle), min in/out = 1
416        // ⇒ cheap short-circuit returns 1. Without checks, the
417        // fixed-vertex loop still finds st_edge_conn(0, v) = 1.
418        assert_eq!(edge_connectivity(&g, true).expect("ec"), 1);
419        assert_eq!(edge_connectivity(&g, false).expect("ec"), 1);
420    }
421
422    // --- 4-cycle with chord: cheap short-circuit fails (min-deg=2,
423    //     not complete in the FL-015 sense), full loop returns 2. ---
424
425    #[test]
426    fn cycle_with_chord_undirected_returns_two() {
427        let mut g = Graph::new(4, false).expect("graph");
428        for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 0), (0, 2)] {
429            g.add_edge(u, v).expect("edge");
430        }
431        // Vertices 1 and 3 have degree 2 — lambda = 2.
432        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
433        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
434    }
435
436    // --- C unit-test fixture parity (igraph_edge_connectivity.c style) ---
437
438    #[test]
439    fn c_fixture_directed_6v_equals_one() {
440        // 6 vertices, 8 directed arcs (mirrors st_edge_connectivity
441        // C fixture). Without a 5→0 back-arc this graph is not
442        // strongly connected — lambda = 0.
443        let mut g = Graph::new(6, true).expect("graph");
444        let arcs = [
445            (0u32, 1u32),
446            (0, 2),
447            (1, 2),
448            (1, 3),
449            (2, 4),
450            (3, 4),
451            (3, 5),
452            (4, 5),
453        ];
454        for (u, v) in arcs {
455            g.add_edge(u, v).expect("edge");
456        }
457        // Source 0 has no incoming, sink 5 has no outgoing —
458        // strongly disconnected.
459        assert_eq!(edge_connectivity(&g, true).expect("ec"), 0);
460        assert_eq!(edge_connectivity(&g, false).expect("ec"), 0);
461    }
462
463    #[test]
464    fn c_fixture_undirected_6v_equals_two() {
465        let mut g = Graph::new(6, false).expect("graph");
466        let arcs = [
467            (0u32, 1u32),
468            (0, 2),
469            (1, 2),
470            (1, 3),
471            (2, 4),
472            (3, 4),
473            (3, 5),
474            (4, 5),
475        ];
476        for (u, v) in arcs {
477            g.add_edge(u, v).expect("edge");
478        }
479        // All vertices have degree ≥ 2; lambda = 2 (e.g. cut {3-5, 4-5}
480        // isolates vertex 5).
481        assert_eq!(edge_connectivity(&g, true).expect("ec"), 2);
482        assert_eq!(edge_connectivity(&g, false).expect("ec"), 2);
483    }
484
485    // --- checks=false vs checks=true agreement ---
486
487    #[test]
488    fn checks_false_matches_checks_true_on_small_graphs() {
489        let fixtures: Vec<Graph> = vec![
490            ring_graph_n(6, false),
491            ring_graph_n(6, true),
492            path_graph_n(5, false),
493            complete_undirected(4),
494            complete_directed_mutual(4),
495        ];
496        for g in fixtures {
497            let with_checks = edge_connectivity(&g, true).expect("ec");
498            let without = edge_connectivity(&g, false).expect("ec");
499            assert_eq!(
500                with_checks,
501                without,
502                "checks={{true,false}} disagree on n={}, dir={}",
503                g.vcount(),
504                g.is_directed()
505            );
506        }
507    }
508
509    // --- adhesion alias parity ---
510
511    #[test]
512    fn adhesion_alias_matches_edge_connectivity() {
513        let fixtures: Vec<Graph> = vec![
514            ring_graph_n(7, false),
515            complete_undirected(5),
516            path_graph_n(4, false),
517        ];
518        for g in fixtures {
519            assert_eq!(
520                edge_connectivity(&g, true).expect("ec"),
521                adhesion(&g, true).expect("ec"),
522                "adhesion/edge_connectivity mismatch on n={}",
523                g.vcount(),
524            );
525        }
526    }
527}
528
529#[cfg(all(test, feature = "proptest-harness"))]
530mod proptests {
531    //! Proptest invariants:
532    //! * `0 <= edge_connectivity <= min_degree` (Whitney 1932).
533    //! * `edge_connectivity <= st_edge_connectivity(s, t)` for every
534    //!   pair `(s, t)`.
535    //! * `checks=true` agrees with `checks=false`.
536    //! * On disconnected graphs (cheap to detect), the result is `0`.
537
538    use super::*;
539    use crate::core::Graph;
540    use proptest::prelude::*;
541
542    fn xorshift(mut r: u64) -> u64 {
543        r ^= r << 13;
544        r ^= r >> 7;
545        r ^= r << 17;
546        r
547    }
548
549    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
550        let mut g = Graph::new(n, directed).expect("graph");
551        let mut state = seed | 1;
552        for _ in 0..m_max {
553            state = xorshift(state);
554            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
555            state = xorshift(state);
556            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
557            if u == v {
558                continue;
559            }
560            g.add_edge(u, v).expect("edge");
561        }
562        g
563    }
564
565    proptest! {
566        #[test]
567        fn ec_nonnegative_and_bounded_by_n_minus_one(
568            seed in any::<u64>(),
569            n in 2u32..7,
570            m in 0u32..14,
571            directed in any::<bool>(),
572        ) {
573            let g = build_random(seed, n, m, directed);
574            let ec = edge_connectivity(&g, true).expect("ec");
575            prop_assert!(ec >= 0, "ec must be non-negative, got {ec}");
576            // ec is unbounded above by n - 1 for multigraphs, but is
577            // bounded by m. (Multigraphs from our random builder can
578            // produce parallel edges.)
579            prop_assert!(ec as u64 <= u64::from(m),
580                "ec={ec} > m={m} (n={n}, seed={seed})");
581        }
582
583        #[test]
584        fn checks_true_matches_checks_false(
585            seed in any::<u64>(),
586            n in 2u32..6,
587            m in 0u32..12,
588            directed in any::<bool>(),
589        ) {
590            let g = build_random(seed, n, m, directed);
591            let with_checks = edge_connectivity(&g, true).expect("ec");
592            let without = edge_connectivity(&g, false).expect("ec");
593            prop_assert_eq!(with_checks, without,
594                "checks=true {} != checks=false {} (n={}, m={}, directed={}, seed={})",
595                with_checks, without, n, m, directed, seed);
596        }
597
598        #[test]
599        fn ec_bounded_by_min_degree_undirected(
600            seed in any::<u64>(),
601            n in 3u32..6,
602            m in 1u32..10,
603        ) {
604            // Whitney: lambda(G) <= min-degree(G).
605            let g = build_random(seed, n, m, false);
606            let mut min_deg = u32::MAX;
607            for v in 0..n {
608                let d = u32::try_from(g.degree(v).expect("degree")).unwrap_or(u32::MAX);
609                if d < min_deg { min_deg = d; }
610            }
611            let ec = edge_connectivity(&g, true).expect("ec");
612            prop_assert!(ec <= i64::from(min_deg),
613                "ec={ec} > min_deg={min_deg} (n={n}, m={m}, seed={seed})");
614        }
615
616        #[test]
617        fn ec_bounded_by_any_pair_st_edge_connectivity(
618            seed in any::<u64>(),
619            n in 3u32..5,
620            m in 1u32..9,
621            directed in any::<bool>(),
622        ) {
623            // lambda(G) <= st_edge_connectivity(s, t) for any (s, t).
624            use super::super::st_edge_connectivity::st_edge_connectivity;
625            let g = build_random(seed, n, m, directed);
626            let ec = edge_connectivity(&g, true).expect("ec");
627            for s in 0..n {
628                for t in 0..n {
629                    if s == t { continue; }
630                    let st = st_edge_connectivity(&g, s, t).expect("st_ec");
631                    prop_assert!(ec <= st,
632                        "ec={ec} > st_ec({s},{t})={st} (n={n}, m={m}, dir={directed}, seed={seed})");
633                }
634            }
635        }
636    }
637}