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}