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