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