rust_igraph/algorithms/flow/vertex_disjoint_paths.rs
1//! `vertex_disjoint_paths` (ALGO-FL-014) — maximum number of
2//! internally vertex-disjoint `source → target` paths.
3//!
4//! Counterpart of `igraph_vertex_disjoint_paths` in
5//! `references/igraph/src/flow/flow.c` (lines 2374-2405). The C source
6//! is a 30-line wrapper that rejects `source == target`, computes the
7//! count of direct `source → target` edges via
8//! `igraph_get_all_eids_between`, then delegates to one of
9//! `igraph_i_st_vertex_connectivity_{directed, undirected}` with
10//! `IGRAPH_VCONN_NEI_IGNORE` (the splitting reduction ignores direct
11//! edges) and adds the direct-edge count back to the result.
12//!
13//! By Menger's theorem (1927) the maximum number of pairwise
14//! internally vertex-disjoint `s → t` paths equals the cardinality of
15//! the minimum s-t internal-vertex cut whenever `s` and `t` are not
16//! adjacent. When they are, the direct-edge count is added on top:
17//! every parallel arc contributes one trivially internally-disjoint
18//! path of length 1.
19//!
20//! Note that `vertex_disjoint_paths(g, s, t) ==
21//! st_vertex_connectivity(g, s, t, VconnNei::Ignore)` exactly when no
22//! direct `source → target` edge exists. When direct edges exist,
23//! `vertex_disjoint_paths` exceeds the `Ignore` value by the
24//! direct-edge count — `VconnNei::NumberOfNodes` is *not* an
25//! equivalent spelling (it returns the sentinel `vcount()` instead of
26//! the additive count). We keep `vertex_disjoint_paths` as a
27//! separate Rust function for naming parity with igraph C, so call
28//! sites can pick the spelling that matches their intent ("paths"
29//! vs. "connectivity").
30
31use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
32
33use super::st_vertex_connectivity::{VconnNei, st_vertex_connectivity};
34
35/// Maximum number of pairwise internally vertex-disjoint paths from
36/// `source` to `target`.
37///
38/// Counterpart of `igraph_vertex_disjoint_paths` in
39/// `references/igraph/src/flow/flow.c:2374`. By Menger's theorem
40/// (1927) on internal-vertex cuts, this equals the s-t vertex
41/// connectivity (under the `Ignore` convention) plus the count of
42/// direct `source → target` edges. See
43/// [`st_vertex_connectivity`](super::st_vertex_connectivity::st_vertex_connectivity)
44/// for the connectivity-flavoured spelling.
45///
46/// # Arguments
47///
48/// * `graph` — input graph (directed or undirected).
49/// * `source` — source vertex id (`0 ≤ source < vcount()`).
50/// * `target` — sink vertex id (`0 ≤ target < vcount()`,
51/// `target != source`).
52///
53/// # Returns
54///
55/// The maximum number of internally vertex-disjoint `source → target`
56/// paths as `i64`, plus the count of direct `source → target` edges.
57/// Returns `0` when no path exists at all.
58///
59/// # Errors
60///
61/// Same as
62/// [`st_vertex_connectivity`](super::st_vertex_connectivity::st_vertex_connectivity):
63///
64/// * [`IgraphError::VertexOutOfRange`] if `source` or `target` is
65/// outside `[0, vcount())`.
66/// * [`IgraphError::InvalidArgument`] if `source == target` (igraph C
67/// returns `IGRAPH_UNIMPLEMENTED` for this case; we use
68/// `InvalidArgument` for parity with the rest of our flow module),
69/// or if `vcount() < 2`.
70/// * [`IgraphError::Internal`] if the sum `vc + direct_count`
71/// overflows `i64` (unreachable in practice — both summands are
72/// bounded above by `ecount() ≤ u32::MAX`).
73///
74/// [`IgraphError::VertexOutOfRange`]: crate::core::IgraphError::VertexOutOfRange
75/// [`IgraphError::InvalidArgument`]: crate::core::IgraphError::InvalidArgument
76/// [`IgraphError::Internal`]: crate::core::IgraphError::Internal
77///
78/// # Examples
79///
80/// ```
81/// use rust_igraph::{Graph, vertex_disjoint_paths};
82///
83/// // Two parallel directed paths 0→1→3 and 0→2→3 → 2 vertex-disjoint paths.
84/// let mut g = Graph::new(4, true).unwrap();
85/// for (s, t) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
86/// g.add_edge(s, t).unwrap();
87/// }
88/// assert_eq!(vertex_disjoint_paths(&g, 0, 3).unwrap(), 2);
89/// ```
90pub fn vertex_disjoint_paths(
91 graph: &Graph,
92 source: VertexId,
93 target: VertexId,
94) -> IgraphResult<i64> {
95 let vc = st_vertex_connectivity(graph, source, target, VconnNei::Ignore)?;
96 let direct = graph.get_all_eids_between(source, target)?.len();
97 let direct_i64 = i64::try_from(direct)
98 .map_err(|_| IgraphError::Internal("direct-edge count exceeds i64::MAX"))?;
99 vc.checked_add(direct_i64).ok_or(IgraphError::Internal(
100 "vertex_disjoint_paths sum overflows i64",
101 ))
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::core::IgraphError;
108
109 #[test]
110 fn rejects_source_equals_target() {
111 let mut g = Graph::new(2, true).expect("graph");
112 g.add_edge(0, 1).expect("edge");
113 let err = vertex_disjoint_paths(&g, 0, 0).unwrap_err();
114 match err {
115 IgraphError::InvalidArgument(_) => {}
116 other => panic!("expected InvalidArgument, got {other:?}"),
117 }
118 }
119
120 #[test]
121 fn rejects_out_of_range_source() {
122 let g = Graph::new(2, true).expect("graph");
123 let err = vertex_disjoint_paths(&g, 5, 0).unwrap_err();
124 match err {
125 IgraphError::VertexOutOfRange { id, n } => {
126 assert_eq!(id, 5);
127 assert_eq!(n, 2);
128 }
129 other => panic!("expected VertexOutOfRange, got {other:?}"),
130 }
131 }
132
133 #[test]
134 fn rejects_out_of_range_target() {
135 let g = Graph::new(2, true).expect("graph");
136 let err = vertex_disjoint_paths(&g, 0, 5).unwrap_err();
137 match err {
138 IgraphError::VertexOutOfRange { id, n } => {
139 assert_eq!(id, 5);
140 assert_eq!(n, 2);
141 }
142 other => panic!("expected VertexOutOfRange, got {other:?}"),
143 }
144 }
145
146 #[test]
147 fn isolated_endpoints_have_no_paths() {
148 let g = Graph::new(4, true).expect("graph");
149 assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 0);
150 }
151
152 #[test]
153 fn single_direct_edge_one_path() {
154 let mut g = Graph::new(2, true).expect("graph");
155 g.add_edge(0, 1).expect("edge");
156 assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 1);
157 }
158
159 #[test]
160 fn two_parallel_directed_paths() {
161 // 0→1→3 and 0→2→3 → 2 internally vertex-disjoint paths
162 // (vertices 1 and 2 each on one path, no shared interior).
163 let mut g = Graph::new(4, true).expect("graph");
164 for (s, t) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
165 g.add_edge(s, t).expect("edge");
166 }
167 assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 2);
168 }
169
170 #[test]
171 fn direct_edge_plus_one_via_interior() {
172 // 0→1 direct, 0→2→1 via vertex 2. 1 direct + 1 internally-disjoint = 2.
173 let mut g = Graph::new(3, true).expect("graph");
174 for (s, t) in [(0u32, 1u32), (0, 2), (2, 1)] {
175 g.add_edge(s, t).expect("edge");
176 }
177 assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 2);
178 }
179
180 #[test]
181 fn parallel_direct_arcs() {
182 // 4 parallel arcs 0→1 → 4 trivially-disjoint paths.
183 let mut g = Graph::new(2, true).expect("graph");
184 for _ in 0..4 {
185 g.add_edge(0, 1).expect("edge");
186 }
187 assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 4);
188 }
189
190 #[test]
191 fn directed_no_reverse_path() {
192 // 0 → 1 → 2 → 3 directed chain.
193 let mut g = Graph::new(4, true).expect("graph");
194 for (s, t) in [(0u32, 1u32), (1, 2), (2, 3)] {
195 g.add_edge(s, t).expect("edge");
196 }
197 assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 1);
198 // Reverse direction has no paths.
199 assert_eq!(vertex_disjoint_paths(&g, 3, 0).expect("paths"), 0);
200 }
201
202 #[test]
203 fn c_unit_fixture_directed() {
204 // Mirrors igraph_vertex_disjoint_paths.c lines 28-39 — directed
205 // 7-vertex graph with self-loop, mutual arcs, and a direct s→t
206 // arc. vdp(0,5)=3, vdp(1,3)=2, vdp(4,0)=0.
207 let mut g = Graph::new(7, true).expect("graph");
208 let arcs = [
209 (0u32, 1u32),
210 (0, 2),
211 (1, 2),
212 (1, 3),
213 (2, 4),
214 (3, 4),
215 (3, 5),
216 (4, 5),
217 (0, 5),
218 (3, 3),
219 (5, 2),
220 (1, 3),
221 (3, 1),
222 ];
223 for (s, t) in arcs {
224 g.add_edge(s, t).expect("edge");
225 }
226 assert_eq!(vertex_disjoint_paths(&g, 0, 5).expect("paths"), 3);
227 assert_eq!(vertex_disjoint_paths(&g, 1, 3).expect("paths"), 2);
228 assert_eq!(vertex_disjoint_paths(&g, 4, 0).expect("paths"), 0);
229 }
230
231 #[test]
232 fn c_unit_fixture_undirected() {
233 // Mirrors igraph_vertex_disjoint_paths.c lines 41-47 — same
234 // graph but converted to undirected. vdp(4,0)=3, vdp(1,3)=5.
235 let mut g = Graph::new(7, false).expect("graph");
236 // The C test calls igraph_to_undirected(IGRAPH_TO_UNDIRECTED_EACH)
237 // which keeps every directed arc as a separate undirected edge.
238 // We faithfully replay the original arc list.
239 let arcs = [
240 (0u32, 1u32),
241 (0, 2),
242 (1, 2),
243 (1, 3),
244 (2, 4),
245 (3, 4),
246 (3, 5),
247 (4, 5),
248 (0, 5),
249 (3, 3),
250 (5, 2),
251 (1, 3),
252 (3, 1),
253 ];
254 for (s, t) in arcs {
255 g.add_edge(s, t).expect("edge");
256 }
257 assert_eq!(vertex_disjoint_paths(&g, 4, 0).expect("paths"), 3);
258 assert_eq!(vertex_disjoint_paths(&g, 1, 3).expect("paths"), 5);
259 }
260
261 #[test]
262 fn vdp_equals_vc_ignore_plus_direct_count_with_direct_edge() {
263 // When a direct s→t edge exists, vdp == vc(Ignore) + direct_count.
264 // VconnNei::NumberOfNodes returns the sentinel vcount() in this
265 // case, so it is NOT equivalent (verified inline below).
266 let mut g = Graph::new(3, true).expect("graph");
267 for (s, t) in [(0u32, 1u32), (0, 2), (2, 1)] {
268 g.add_edge(s, t).expect("edge");
269 }
270 let vdp = vertex_disjoint_paths(&g, 0, 1).expect("vdp");
271 let vc_ign = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
272 let direct =
273 i64::try_from(g.get_all_eids_between(0, 1).expect("eids").len()).expect("direct fits");
274 assert_eq!(vdp, vc_ign + direct);
275
276 // Sentinel sanity-check: NumberOfNodes returns vcount() (=3) here,
277 // confirming it is not interchangeable with vertex_disjoint_paths.
278 let vc_non = st_vertex_connectivity(&g, 0, 1, VconnNei::NumberOfNodes).expect("vc");
279 assert_eq!(vc_non, 3);
280 }
281
282 #[test]
283 fn matches_st_vertex_connectivity_ignore_when_no_direct_edge() {
284 // When no direct s→t edge exists, vdp must equal
285 // st_vertex_connectivity(Ignore) since the direct-edge count is 0.
286 let mut g = Graph::new(6, false).expect("graph");
287 for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 5)] {
288 g.add_edge(u, v).expect("edge");
289 }
290 let vdp = vertex_disjoint_paths(&g, 0, 5).expect("vdp");
291 let vc_ign = st_vertex_connectivity(&g, 0, 5, VconnNei::Ignore).expect("vc");
292 assert_eq!(vdp, vc_ign);
293 }
294}
295
296#[cfg(all(test, feature = "proptest-harness"))]
297mod proptests {
298 //! Proptest cross-validates the Menger equality
299 //! `vertex_disjoint_paths(g, s, t) ==
300 //! st_vertex_connectivity(g, s, t, Ignore) + |direct(s, t)|`
301 //! across random unit-cap graphs. FL-013 itself proptests against
302 //! `st_edge_connectivity` (Whitney bound), so this transitively
303 //! inherits independent oracles.
304
305 use super::*;
306 use crate::core::Graph;
307 use proptest::prelude::*;
308
309 fn xorshift(mut r: u64) -> u64 {
310 r ^= r << 13;
311 r ^= r >> 7;
312 r ^= r << 17;
313 r
314 }
315
316 fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
317 let mut g = Graph::new(n, directed).expect("graph");
318 let mut state = seed | 1;
319 for _ in 0..m_max {
320 state = xorshift(state);
321 let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
322 state = xorshift(state);
323 let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
324 if u == v {
325 continue;
326 }
327 g.add_edge(u, v).expect("edge");
328 }
329 g
330 }
331
332 proptest! {
333 #[test]
334 fn vdp_equals_vc_ignore_plus_direct(
335 seed in any::<u64>(),
336 n in 2u32..7,
337 m in 1u32..12,
338 directed in any::<bool>(),
339 ) {
340 let g = build_random(seed, n, m, directed);
341 let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
342 let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
343 let t = if t_raw == s { (s + 1) % n } else { t_raw };
344 prop_assume!(s != t);
345
346 let vdp = vertex_disjoint_paths(&g, s, t).expect("vdp");
347 let vc = st_vertex_connectivity(&g, s, t, VconnNei::Ignore).expect("vc");
348 let direct = i64::try_from(g.get_all_eids_between(s, t).expect("eids").len())
349 .expect("direct fits");
350 prop_assert_eq!(
351 vdp,
352 vc + direct,
353 "vdp={} vc(Ignore)={} direct={} (n={}, m={}, directed={}, seed={})",
354 vdp, vc, direct, n, m, directed, seed
355 );
356 }
357
358 #[test]
359 fn vdp_le_n(
360 seed in any::<u64>(),
361 n in 2u32..7,
362 m in 1u32..12,
363 directed in any::<bool>(),
364 ) {
365 // Trivially-disjoint paths can't exceed parallel-edge count
366 // plus internal vertex slack, both bounded by graph size.
367 // Use ecount as a slack-free upper bound (an edge per path).
368 let g = build_random(seed, n, m, directed);
369 let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
370 let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
371 let t = if t_raw == s { (s + 1) % n } else { t_raw };
372 prop_assume!(s != t);
373
374 let vdp = vertex_disjoint_paths(&g, s, t).expect("vdp");
375 let ec_bound = i64::try_from(g.ecount()).expect("ecount fits i64");
376 prop_assert!(vdp <= ec_bound, "vdp={} > ecount={}", vdp, ec_bound);
377 }
378 }
379}