1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
48
49use super::max_flow::max_flow_value;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum VconnNei {
59 Error,
62 Negative,
65 NumberOfNodes,
69 Ignore,
74}
75
76pub fn st_vertex_connectivity(
132 graph: &Graph,
133 source: VertexId,
134 target: VertexId,
135 mode: VconnNei,
136) -> IgraphResult<i64> {
137 let n = graph.vcount();
138
139 if n < 2 {
140 return Err(IgraphError::InvalidArgument(format!(
141 "st_vertex_connectivity needs at least 2 vertices, graph has {n}"
142 )));
143 }
144 if source >= n {
145 return Err(IgraphError::VertexOutOfRange { id: source, n });
146 }
147 if target >= n {
148 return Err(IgraphError::VertexOutOfRange { id: target, n });
149 }
150 if source == target {
151 return Err(IgraphError::InvalidArgument(format!(
152 "source and target vertices are the same ({source})"
153 )));
154 }
155
156 let direct_eids = graph.get_all_eids_between(source, target)?;
160 let no_conn = direct_eids.len();
161 let has_direct = no_conn > 0;
162
163 match mode {
164 VconnNei::Error if has_direct => {
165 return Err(IgraphError::InvalidArgument(format!(
166 "source ({source}) and target ({target}) are directly connected"
167 )));
168 }
169 VconnNei::Negative if has_direct => return Ok(-1),
170 VconnNei::NumberOfNodes if has_direct => return Ok(i64::from(n)),
171 _ => {}
172 }
173
174 let split_n = n
183 .checked_mul(2)
184 .ok_or(IgraphError::Internal("split-vertex graph overflowed u32"))?;
185 let mut split = Graph::new(split_n, true)?;
186
187 let mut original_arc_count: usize = 0;
188 if graph.is_directed() {
189 for eid in 0..graph.ecount() {
190 let eid_u32 =
191 u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow u32"))?;
192 let (u, v) = graph.edge(eid_u32)?;
193 split.add_edge(u, v + n)?;
194 original_arc_count += 1;
195 }
196 } else {
197 for eid in 0..graph.ecount() {
201 let eid_u32 =
202 u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow u32"))?;
203 let (u, v) = graph.edge(eid_u32)?;
204 split.add_edge(u, v + n)?;
205 split.add_edge(v, u + n)?;
206 original_arc_count += 2;
207 }
208 }
209 for v in 0..n {
210 split.add_edge(v + n, v)?;
211 }
212
213 let split_ecount = split.ecount();
219 let mut caps = vec![1.0f64; split_ecount];
220 let source_in = source + n;
221 let target_out = target;
222 for eid in split.incident(source_in)? {
223 caps[eid as usize] = 0.0;
224 }
225 for eid in split.incident(target_out)? {
226 caps[eid as usize] = 0.0;
227 }
228
229 let _ = original_arc_count; let flow = max_flow_value(&split, source, target + n, Some(&caps))?;
235
236 #[allow(
237 clippy::cast_precision_loss,
238 clippy::cast_possible_truncation,
239 clippy::cast_sign_loss
240 )]
241 let raw = {
242 if !flow.is_finite() || flow < 0.0 || flow > i64::MAX as f64 {
243 return Err(IgraphError::Internal(
244 "unit-capacity max-flow value is not representable as i64",
245 ));
246 }
247 flow.round() as i64
248 };
249
250 let no_conn_i64 = i64::try_from(no_conn).map_err(|_| {
251 IgraphError::Internal("number of parallel direct edges does not fit in i64")
252 })?;
253
254 Ok(raw - no_conn_i64)
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::core::IgraphError;
261
262 #[test]
265 fn rejects_source_equals_target() {
266 let mut g = Graph::new(2, false).expect("graph");
267 g.add_edge(0, 1).expect("edge");
268 let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
269 assert!(matches!(err, IgraphError::InvalidArgument(_)));
270 }
271
272 #[test]
273 fn rejects_empty_graph() {
274 let g = Graph::new(0, false).expect("graph");
275 let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
276 assert!(matches!(err, IgraphError::InvalidArgument(_)));
277 }
278
279 #[test]
280 fn rejects_single_vertex_graph() {
281 let g = Graph::new(1, false).expect("graph");
282 let err = st_vertex_connectivity(&g, 0, 0, VconnNei::Error).unwrap_err();
283 assert!(matches!(err, IgraphError::InvalidArgument(_)));
284 }
285
286 #[test]
287 fn rejects_out_of_range_source() {
288 let g = Graph::new(3, false).expect("graph");
289 let err = st_vertex_connectivity(&g, 5, 0, VconnNei::Error).unwrap_err();
290 match err {
291 IgraphError::VertexOutOfRange { id, n } => {
292 assert_eq!(id, 5);
293 assert_eq!(n, 3);
294 }
295 other => panic!("expected VertexOutOfRange, got {other:?}"),
296 }
297 }
298
299 #[test]
300 fn rejects_out_of_range_target() {
301 let g = Graph::new(3, false).expect("graph");
302 let err = st_vertex_connectivity(&g, 0, 5, VconnNei::Error).unwrap_err();
303 match err {
304 IgraphError::VertexOutOfRange { id, n } => {
305 assert_eq!(id, 5);
306 assert_eq!(n, 3);
307 }
308 other => panic!("expected VertexOutOfRange, got {other:?}"),
309 }
310 }
311
312 #[test]
315 fn two_unconnected_vertices_error_mode_returns_zero() {
316 let g = Graph::new(2, false).expect("graph");
317 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Error).expect("vc");
318 assert_eq!(v, 0);
319 }
320
321 #[test]
322 fn two_connected_vertices_negative_mode_returns_negative_one() {
323 let mut g = Graph::new(2, false).expect("graph");
324 g.add_edge(0, 1).expect("edge");
325 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Negative).expect("vc");
326 assert_eq!(v, -1);
327 }
328
329 #[test]
330 fn two_connected_vertices_number_of_nodes_mode_returns_n() {
331 let mut g = Graph::new(2, false).expect("graph");
332 g.add_edge(0, 1).expect("edge");
333 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::NumberOfNodes).expect("vc");
334 assert_eq!(v, 2);
335 }
336
337 #[test]
338 fn three_parallel_undirected_edges_ignore_mode_returns_zero() {
339 let mut g = Graph::new(2, false).expect("graph");
340 for _ in 0..3 {
341 g.add_edge(0, 1).expect("edge");
342 }
343 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
344 assert_eq!(v, 0);
345 }
346
347 #[test]
348 fn mixed_parallel_undirected_edges_ignore_mode_returns_zero() {
349 let mut g = Graph::new(2, false).expect("graph");
353 for _ in 0..3 {
354 g.add_edge(0, 1).expect("edge");
355 }
356 for _ in 0..2 {
357 g.add_edge(1, 0).expect("edge");
358 }
359 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
360 assert_eq!(v, 0);
361 }
362
363 #[test]
364 fn line_graph_6v_error_mode_returns_one() {
365 let mut g = Graph::new(6, false).expect("graph");
368 for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 5)] {
369 g.add_edge(u, v).expect("edge");
370 }
371 let v = st_vertex_connectivity(&g, 0, 5, VconnNei::Error).expect("vc");
372 assert_eq!(v, 1);
373 }
374
375 #[test]
376 fn full_graph_6v_undirected_ignore_mode_returns_four() {
377 let mut g = Graph::new(6, false).expect("graph");
380 for i in 0u32..6 {
381 for j in (i + 1)..6 {
382 g.add_edge(i, j).expect("edge");
383 }
384 }
385 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
386 assert_eq!(v, 4);
387 }
388
389 #[test]
390 fn full_graph_6v_directed_ignore_mode_returns_four() {
391 let mut g = Graph::new(6, true).expect("graph");
393 for i in 0u32..6 {
394 for j in 0u32..6 {
395 if i != j {
396 g.add_edge(i, j).expect("edge");
397 }
398 }
399 }
400 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
401 assert_eq!(v, 4);
402 }
403
404 #[test]
405 fn three_vertex_bottleneck_error_mode_returns_one() {
406 let mut g = Graph::new(3, false).expect("graph");
409 for _ in 0..2 {
410 g.add_edge(0, 1).expect("edge");
411 }
412 for _ in 0..4 {
413 g.add_edge(1, 2).expect("edge");
414 }
415 let v = st_vertex_connectivity(&g, 0, 2, VconnNei::Error).expect("vc");
416 assert_eq!(v, 1);
417 }
418
419 #[test]
422 fn isolated_endpoints_have_zero_connectivity() {
423 let g = Graph::new(4, true).expect("graph");
424 let v = st_vertex_connectivity(&g, 0, 3, VconnNei::Error).expect("vc");
425 assert_eq!(v, 0);
426 }
427
428 #[test]
429 fn two_internally_vertex_disjoint_paths() {
430 let mut g = Graph::new(4, true).expect("graph");
432 for (u, v) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
433 g.add_edge(u, v).expect("edge");
434 }
435 let v = st_vertex_connectivity(&g, 0, 3, VconnNei::Error).expect("vc");
436 assert_eq!(v, 2);
437 }
438
439 #[test]
440 fn directed_anti_parallel_does_not_count_in_error_mode() {
441 let mut g = Graph::new(2, true).expect("graph");
444 g.add_edge(0, 1).expect("edge");
445 g.add_edge(1, 0).expect("edge");
446 let err = st_vertex_connectivity(&g, 0, 1, VconnNei::Error).unwrap_err();
447 assert!(matches!(err, IgraphError::InvalidArgument(_)));
448 }
449
450 #[test]
451 fn ignore_mode_subtracts_parallel_direct_arcs_directed() {
452 let mut g = Graph::new(3, true).expect("graph");
456 g.add_edge(0, 1).expect("edge");
457 g.add_edge(0, 2).expect("edge");
458 g.add_edge(2, 1).expect("edge");
459 let v = st_vertex_connectivity(&g, 0, 1, VconnNei::Ignore).expect("vc");
460 assert_eq!(v, 1);
461 }
462}
463
464#[cfg(all(test, feature = "proptest-harness"))]
465mod proptests {
466 use super::*;
471 use crate::algorithms::flow::st_edge_connectivity::st_edge_connectivity;
472 use crate::core::Graph;
473 use proptest::prelude::*;
474
475 fn xorshift(mut r: u64) -> u64 {
476 r ^= r << 13;
477 r ^= r >> 7;
478 r ^= r << 17;
479 r
480 }
481
482 fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
483 let mut g = Graph::new(n, directed).expect("graph");
484 let mut state = seed | 1;
485 for _ in 0..m_max {
486 state = xorshift(state);
487 let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
488 state = xorshift(state);
489 let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
490 if u == v {
491 continue;
492 }
493 g.add_edge(u, v).expect("edge");
494 }
495 g
496 }
497
498 proptest! {
499 #[test]
500 fn vc_le_ec_when_no_direct_edge(
501 seed in any::<u64>(),
502 n in 3u32..8,
503 m in 1u32..16,
504 directed in any::<bool>(),
505 ) {
506 let g = build_random(seed, n, m, directed);
509 let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
510 let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
511 let t = if t_raw == s { (s + 1) % n } else { t_raw };
512 prop_assume!(s != t);
513
514 let has_direct = if directed {
518 g.find_eid(s, t).expect("find").is_some()
519 } else {
520 !g.get_all_eids_between(s, t).expect("eids").is_empty()
521 };
522 prop_assume!(!has_direct);
523
524 let vc = st_vertex_connectivity(&g, s, t, VconnNei::Error)
525 .expect("vc (no direct edge by prop_assume)");
526 let ec = st_edge_connectivity(&g, s, t).expect("ec");
527
528 prop_assert!(vc >= 0,
529 "vc must be non-negative without direct edge, got {vc}");
530 prop_assert!(vc <= ec,
531 "Whitney violated: vc={vc} > ec={ec} (n={n}, m={m}, directed={directed}, seed={seed})");
532 prop_assert!(vc <= i64::from(n) - 2,
534 "vc={vc} exceeds n-2={} (n={n})", i64::from(n) - 2);
535 }
536
537 #[test]
538 fn ignore_mode_is_non_negative(
539 seed in any::<u64>(),
540 n in 2u32..6,
541 m in 0u32..12,
542 directed in any::<bool>(),
543 ) {
544 let g = build_random(seed, n, m, directed);
545 let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
546 let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
547 let t = if t_raw == s { (s + 1) % n } else { t_raw };
548 prop_assume!(s != t);
549
550 let vc = st_vertex_connectivity(&g, s, t, VconnNei::Ignore).expect("vc");
551 prop_assert!(vc >= 0,
552 "Ignore mode returned negative: vc={vc} (n={n}, m={m}, directed={directed}, seed={seed})");
553 }
554 }
555}