use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
use super::max_flow::max_flow_value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VconnNei {
Error,
Negative,
NumberOfNodes,
Ignore,
}
pub fn st_vertex_connectivity(
graph: &Graph,
source: VertexId,
target: VertexId,
mode: VconnNei,
) -> IgraphResult<i64> {
let n = graph.vcount();
if n < 2 {
return Err(IgraphError::InvalidArgument(format!(
"st_vertex_connectivity needs at least 2 vertices, graph has {n}"
)));
}
if source >= n {
return Err(IgraphError::VertexOutOfRange { id: source, n });
}
if target >= n {
return Err(IgraphError::VertexOutOfRange { id: target, n });
}
if source == target {
return Err(IgraphError::InvalidArgument(format!(
"source and target vertices are the same ({source})"
)));
}
let direct_eids = graph.get_all_eids_between(source, target)?;
let no_conn = direct_eids.len();
let has_direct = no_conn > 0;
match mode {
VconnNei::Error if has_direct => {
return Err(IgraphError::InvalidArgument(format!(
"source ({source}) and target ({target}) are directly connected"
)));
}
VconnNei::Negative if has_direct => return Ok(-1),
VconnNei::NumberOfNodes if has_direct => return Ok(i64::from(n)),
_ => {}
}
let split_n = n
.checked_mul(2)
.ok_or(IgraphError::Internal("split-vertex graph overflowed u32"))?;
let mut split = Graph::new(split_n, true)?;
let mut original_arc_count: usize = 0;
if graph.is_directed() {
for eid in 0..graph.ecount() {
let eid_u32 =
u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow u32"))?;
let (u, v) = graph.edge(eid_u32)?;
split.add_edge(u, v + n)?;
original_arc_count += 1;
}
} else {
for eid in 0..graph.ecount() {
let eid_u32 =
u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow u32"))?;
let (u, v) = graph.edge(eid_u32)?;
split.add_edge(u, v + n)?;
split.add_edge(v, u + n)?;
original_arc_count += 2;
}
}
for v in 0..n {
split.add_edge(v + n, v)?;
}
let split_ecount = split.ecount();
let mut caps = vec![1.0f64; split_ecount];
let source_in = source + n;
let target_out = target;
for eid in split.incident(source_in)? {
caps[eid as usize] = 0.0;
}
for eid in split.incident(target_out)? {
caps[eid as usize] = 0.0;
}
let _ = original_arc_count;
let flow = max_flow_value(&split, source, target + n, Some(&caps))?;
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let raw = {
if !flow.is_finite() || flow < 0.0 || flow > i64::MAX as f64 {
return Err(IgraphError::Internal(
"unit-capacity max-flow value is not representable as i64",
));
}
flow.round() as i64
};
let no_conn_i64 = i64::try_from(no_conn).map_err(|_| {
IgraphError::Internal("number of parallel direct edges does not fit in i64")
})?;
Ok(raw - no_conn_i64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::IgraphError;
#[test]
fn rejects_source_equals_target() {
let mut g = Graph::new(2, false).expect("graph");
g.add_edge(0, 1).expect("edge");
let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn rejects_empty_graph() {
let g = Graph::new(0, false).expect("graph");
let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn rejects_single_vertex_graph() {
let g = Graph::new(1, false).expect("graph");
let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn rejects_out_of_range_source() {
let g = Graph::new(3, false).expect("graph");
let err = st_vertex_connectivity(&g, 5, 0, VconnNei::Error).unwrap_err();
match err {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!(id, 5);
assert_eq!(n, 3);
}
other => panic!("expected VertexOutOfRange, got {other:?}"),
}
}
#[test]
fn rejects_out_of_range_target() {
let g = Graph::new(3, false).expect("graph");
let err = st_vertex_connectivity(&g, 0, 5, VconnNei::Error).unwrap_err();
match err {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!(id, 5);
assert_eq!(n, 3);
}
other => panic!("expected VertexOutOfRange, got {other:?}"),
}
}
#[test]
fn two_unconnected_vertices_error_mode_returns_zero() {
let g = Graph::new(2, false).expect("graph");
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Error).expect("vc");
assert_eq!(v, 0);
}
#[test]
fn two_connected_vertices_negative_mode_returns_negative_one() {
let mut g = Graph::new(2, false).expect("graph");
g.add_edge(0, 1).expect("edge");
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Negative).expect("vc");
assert_eq!(v, -1);
}
#[test]
fn two_connected_vertices_number_of_nodes_mode_returns_n() {
let mut g = Graph::new(2, false).expect("graph");
g.add_edge(0, 1).expect("edge");
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::NumberOfNodes).expect("vc");
assert_eq!(v, 2);
}
#[test]
fn three_parallel_undirected_edges_ignore_mode_returns_zero() {
let mut g = Graph::new(2, false).expect("graph");
for _ in 0..3 {
g.add_edge(0, 1).expect("edge");
}
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
assert_eq!(v, 0);
}
#[test]
fn mixed_parallel_undirected_edges_ignore_mode_returns_zero() {
let mut g = Graph::new(2, false).expect("graph");
for _ in 0..3 {
g.add_edge(0, 1).expect("edge");
}
for _ in 0..2 {
g.add_edge(1, 0).expect("edge");
}
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
assert_eq!(v, 0);
}
#[test]
fn line_graph_6v_error_mode_returns_one() {
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 v = st_vertex_connectivity(&g, 0, 5, VconnNei::Error).expect("vc");
assert_eq!(v, 1);
}
#[test]
fn full_graph_6v_undirected_ignore_mode_returns_four() {
let mut g = Graph::new(6, false).expect("graph");
for i in 0u32..6 {
for j in (i + 1)..6 {
g.add_edge(i, j).expect("edge");
}
}
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
assert_eq!(v, 4);
}
#[test]
fn full_graph_6v_directed_ignore_mode_returns_four() {
let mut g = Graph::new(6, true).expect("graph");
for i in 0u32..6 {
for j in 0u32..6 {
if i != j {
g.add_edge(i, j).expect("edge");
}
}
}
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
assert_eq!(v, 4);
}
#[test]
fn three_vertex_bottleneck_error_mode_returns_one() {
let mut g = Graph::new(3, false).expect("graph");
for _ in 0..2 {
g.add_edge(0, 1).expect("edge");
}
for _ in 0..4 {
g.add_edge(1, 2).expect("edge");
}
let v = st_vertex_connectivity(&g, 0, 2, VconnNei::Error).expect("vc");
assert_eq!(v, 1);
}
#[test]
fn isolated_endpoints_have_zero_connectivity() {
let g = Graph::new(4, true).expect("graph");
let v = st_vertex_connectivity(&g, 0, 3, VconnNei::Error).expect("vc");
assert_eq!(v, 0);
}
#[test]
fn two_internally_vertex_disjoint_paths() {
let mut g = Graph::new(4, true).expect("graph");
for (u, v) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
g.add_edge(u, v).expect("edge");
}
let v = st_vertex_connectivity(&g, 0, 3, VconnNei::Error).expect("vc");
assert_eq!(v, 2);
}
#[test]
fn directed_anti_parallel_does_not_count_in_error_mode() {
let mut g = Graph::new(2, true).expect("graph");
g.add_edge(0, 1).expect("edge");
g.add_edge(1, 0).expect("edge");
let err = st_vertex_connectivity(&g, 0, 1, VconnNei::Error).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn ignore_mode_subtracts_parallel_direct_arcs_directed() {
let mut g = Graph::new(3, true).expect("graph");
g.add_edge(0, 1).expect("edge");
g.add_edge(0, 2).expect("edge");
g.add_edge(2, 1).expect("edge");
let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
assert_eq!(v, 1);
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use crate::algorithms::flow::st_edge_connectivity::st_edge_connectivity;
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 vc_le_ec_when_no_direct_edge(
seed in any::<u64>(),
n in 3u32..8,
m in 1u32..16,
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 has_direct = if directed {
g.find_eid(s, t).expect("find").is_some()
} else {
!g.get_all_eids_between(s, t).expect("eids").is_empty()
};
prop_assume!(!has_direct);
let vc = st_vertex_connectivity(&g, s, t, VconnNei::Error)
.expect("vc (no direct edge by prop_assume)");
let ec = st_edge_connectivity(&g, s, t).expect("ec");
prop_assert!(vc >= 0,
"vc must be non-negative without direct edge, got {vc}");
prop_assert!(vc <= ec,
"Whitney violated: vc={vc} > ec={ec} (n={n}, m={m}, directed={directed}, seed={seed})");
prop_assert!(vc <= i64::from(n) - 2,
"vc={vc} exceeds n-2={} (n={n})", i64::from(n) - 2);
}
#[test]
fn ignore_mode_is_non_negative(
seed in any::<u64>(),
n in 2u32..6,
m in 0u32..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 vc = st_vertex_connectivity(&g, s, t, VconnNei::Ignore).expect("vc");
prop_assert!(vc >= 0,
"Ignore mode returned negative: vc={vc} (n={n}, m={m}, directed={directed}, seed={seed})");
}
}
}