Skip to main content

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}