use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
use super::st_vertex_connectivity::{VconnNei, st_vertex_connectivity};
pub fn vertex_disjoint_paths(
graph: &Graph,
source: VertexId,
target: VertexId,
) -> IgraphResult<i64> {
let vc = st_vertex_connectivity(graph, source, target, VconnNei::Ignore)?;
let direct = graph.get_all_eids_between(source, target)?.len();
let direct_i64 = i64::try_from(direct)
.map_err(|_| IgraphError::Internal("direct-edge count exceeds i64::MAX"))?;
vc.checked_add(direct_i64).ok_or(IgraphError::Internal(
"vertex_disjoint_paths sum overflows i64",
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::IgraphError;
#[test]
fn rejects_source_equals_target() {
let mut g = Graph::new(2, true).expect("graph");
g.add_edge(0, 1).expect("edge");
let err = vertex_disjoint_paths(&g, 0, 0).unwrap_err();
match err {
IgraphError::InvalidArgument(_) => {}
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
fn rejects_out_of_range_source() {
let g = Graph::new(2, true).expect("graph");
let err = vertex_disjoint_paths(&g, 5, 0).unwrap_err();
match err {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!(id, 5);
assert_eq!(n, 2);
}
other => panic!("expected VertexOutOfRange, got {other:?}"),
}
}
#[test]
fn rejects_out_of_range_target() {
let g = Graph::new(2, true).expect("graph");
let err = vertex_disjoint_paths(&g, 0, 5).unwrap_err();
match err {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!(id, 5);
assert_eq!(n, 2);
}
other => panic!("expected VertexOutOfRange, got {other:?}"),
}
}
#[test]
fn isolated_endpoints_have_no_paths() {
let g = Graph::new(4, true).expect("graph");
assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 0);
}
#[test]
fn single_direct_edge_one_path() {
let mut g = Graph::new(2, true).expect("graph");
g.add_edge(0, 1).expect("edge");
assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 1);
}
#[test]
fn two_parallel_directed_paths() {
let mut g = Graph::new(4, true).expect("graph");
for (s, t) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
g.add_edge(s, t).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 2);
}
#[test]
fn direct_edge_plus_one_via_interior() {
let mut g = Graph::new(3, true).expect("graph");
for (s, t) in [(0u32, 1u32), (0, 2), (2, 1)] {
g.add_edge(s, t).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 2);
}
#[test]
fn parallel_direct_arcs() {
let mut g = Graph::new(2, true).expect("graph");
for _ in 0..4 {
g.add_edge(0, 1).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 0, 1).expect("paths"), 4);
}
#[test]
fn directed_no_reverse_path() {
let mut g = Graph::new(4, true).expect("graph");
for (s, t) in [(0u32, 1u32), (1, 2), (2, 3)] {
g.add_edge(s, t).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 0, 3).expect("paths"), 1);
assert_eq!(vertex_disjoint_paths(&g, 3, 0).expect("paths"), 0);
}
#[test]
fn c_unit_fixture_directed() {
let mut g = Graph::new(7, true).expect("graph");
let arcs = [
(0u32, 1u32),
(0, 2),
(1, 2),
(1, 3),
(2, 4),
(3, 4),
(3, 5),
(4, 5),
(0, 5),
(3, 3),
(5, 2),
(1, 3),
(3, 1),
];
for (s, t) in arcs {
g.add_edge(s, t).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 0, 5).expect("paths"), 3);
assert_eq!(vertex_disjoint_paths(&g, 1, 3).expect("paths"), 2);
assert_eq!(vertex_disjoint_paths(&g, 4, 0).expect("paths"), 0);
}
#[test]
fn c_unit_fixture_undirected() {
let mut g = Graph::new(7, false).expect("graph");
let arcs = [
(0u32, 1u32),
(0, 2),
(1, 2),
(1, 3),
(2, 4),
(3, 4),
(3, 5),
(4, 5),
(0, 5),
(3, 3),
(5, 2),
(1, 3),
(3, 1),
];
for (s, t) in arcs {
g.add_edge(s, t).expect("edge");
}
assert_eq!(vertex_disjoint_paths(&g, 4, 0).expect("paths"), 3);
assert_eq!(vertex_disjoint_paths(&g, 1, 3).expect("paths"), 5);
}
#[test]
fn vdp_equals_vc_ignore_plus_direct_count_with_direct_edge() {
let mut g = Graph::new(3, true).expect("graph");
for (s, t) in [(0u32, 1u32), (0, 2), (2, 1)] {
g.add_edge(s, t).expect("edge");
}
let vdp = vertex_disjoint_paths(&g, 0, 1).expect("vdp");
let vc_ign = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
let direct =
i64::try_from(g.get_all_eids_between(0, 1).expect("eids").len()).expect("direct fits");
assert_eq!(vdp, vc_ign + direct);
let vc_non = st_vertex_connectivity(&g, 0, 1, VconnNei::NumberOfNodes).expect("vc");
assert_eq!(vc_non, 3);
}
#[test]
fn matches_st_vertex_connectivity_ignore_when_no_direct_edge() {
let mut g = Graph::new(6, false).expect("graph");
for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 5)] {
g.add_edge(u, v).expect("edge");
}
let vdp = vertex_disjoint_paths(&g, 0, 5).expect("vdp");
let vc_ign = st_vertex_connectivity(&g, 0, 5, VconnNei::Ignore).expect("vc");
assert_eq!(vdp, vc_ign);
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use crate::core::Graph;
use proptest::prelude::*;
fn xorshift(mut r: u64) -> u64 {
r ^= r << 13;
r ^= r >> 7;
r ^= r << 17;
r
}
fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
let mut g = Graph::new(n, directed).expect("graph");
let mut state = seed | 1;
for _ in 0..m_max {
state = xorshift(state);
let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
state = xorshift(state);
let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
if u == v {
continue;
}
g.add_edge(u, v).expect("edge");
}
g
}
proptest! {
#[test]
fn vdp_equals_vc_ignore_plus_direct(
seed in any::<u64>(),
n in 2u32..7,
m in 1u32..12,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
let t = if t_raw == s { (s + 1) % n } else { t_raw };
prop_assume!(s != t);
let vdp = vertex_disjoint_paths(&g, s, t).expect("vdp");
let vc = st_vertex_connectivity(&g, s, t, VconnNei::Ignore).expect("vc");
let direct = i64::try_from(g.get_all_eids_between(s, t).expect("eids").len())
.expect("direct fits");
prop_assert_eq!(
vdp,
vc + direct,
"vdp={} vc(Ignore)={} direct={} (n={}, m={}, directed={}, seed={})",
vdp, vc, direct, n, m, directed, seed
);
}
#[test]
fn vdp_le_n(
seed in any::<u64>(),
n in 2u32..7,
m in 1u32..12,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
let t = if t_raw == s { (s + 1) % n } else { t_raw };
prop_assume!(s != t);
let vdp = vertex_disjoint_paths(&g, s, t).expect("vdp");
let ec_bound = i64::try_from(g.ecount()).expect("ecount fits i64");
prop_assert!(vdp <= ec_bound, "vdp={} > ecount={}", vdp, ec_bound);
}
}
}