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}