#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
use crate::algorithms::flow::dominator_tree::{DominatorMode, DominatorTree, dominator_tree};
use crate::algorithms::flow::provan_shier::{EStack, MarkedQueue, Pivot, provan_shier_list};
use crate::algorithms::operators::induced_subgraph::{InducedSubgraphResult, induced_subgraph};
use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StCuts {
pub cuts: Vec<Vec<EdgeId>>,
pub partition1s: Vec<Vec<VertexId>>,
}
pub fn all_st_cuts(graph: &Graph, source: VertexId, target: VertexId) -> IgraphResult<StCuts> {
let n = graph.vcount();
if !graph.is_directed() {
return Err(IgraphError::InvalidArgument(
"all_st_cuts: listing all s-t cuts is only implemented for directed graphs".to_string(),
));
}
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(
"all_st_cuts: source and target must differ".to_string(),
));
}
let n_us = n as usize;
let mut pivot_fn = AllCutsPivot { graph };
let mut partitions = provan_shier_list(n_us, source, target, &mut pivot_fn)?;
let mut cuts: Vec<Vec<EdgeId>> = Vec::with_capacity(partitions.len());
let mut in_s = vec![false; n_us];
for part in &mut partitions {
part.sort_unstable();
for &v in part.iter() {
in_s[v as usize] = true;
}
let mut cut: Vec<EdgeId> = Vec::new();
for e in 0..graph.ecount() {
let (from, to) = graph.edge(e as EdgeId)?;
if in_s[from as usize] && !in_s[to as usize] {
cut.push(e as EdgeId);
}
}
cut.sort_unstable();
cuts.push(cut);
for &v in part.iter() {
in_s[v as usize] = false;
}
}
Ok(StCuts {
cuts,
partition1s: partitions,
})
}
struct AllCutsPivot<'a> {
graph: &'a Graph,
}
impl Pivot for AllCutsPivot<'_> {
fn pivot(
&mut self,
s: &MarkedQueue,
t: &EStack,
source: VertexId,
target: VertexId,
) -> IgraphResult<(VertexId, Vec<VertexId>)> {
pivot(self.graph, s, t, source, target)
}
}
#[allow(clippy::many_single_char_names)]
fn pivot(
graph: &Graph,
s: &MarkedQueue,
t: &EStack,
source: VertexId,
target: VertexId,
) -> IgraphResult<(VertexId, Vec<VertexId>)> {
let n = graph.vcount() as usize;
let mut keep: Vec<VertexId> = Vec::new();
for i in 0..n {
if !s.iselement(i as VertexId) {
keep.push(i as VertexId);
}
}
let sbar: InducedSubgraphResult = induced_subgraph(graph, &keep)?;
let root = sbar.map[target as usize];
let dt = dominator_tree(&sbar.graph, root, DominatorMode::In)?;
let mut gamma_s = vec![false; n];
if s.size() == 0 {
gamma_s[source as usize] = true;
} else {
for i in 0..n {
if s.iselement(i as VertexId) {
for nei in graph.out_neighbors_vec(i as VertexId)? {
if !s.iselement(nei) {
gamma_s[nei as usize] = true;
}
}
}
}
}
let mut leftout_old: Vec<VertexId> = Vec::with_capacity(dt.leftout.len());
for &lo in &dt.leftout {
let old = sbar.invmap[lo as usize];
leftout_old.push(old);
gamma_s[old as usize] = false;
}
let m = if dt.tree.ecount() > 0 {
minimal_gamma(&dt, &sbar, &gamma_s, n)
} else {
Vec::new()
};
if m.is_empty() {
return Ok((0, Vec::new()));
}
let sbar_n = sbar.graph.vcount() as usize;
let mut children: Vec<Vec<u32>> = vec![Vec::new(); sbar_n];
for v in 0..sbar_n {
let p = dt.idom[v];
if p >= 0 {
children[p as usize].push(v as u32);
}
}
let gamma_s_vec: Vec<VertexId> = (0..n)
.filter(|&i| gamma_s[i])
.map(|i| i as VertexId)
.collect();
for &m_old in &m {
let min_new = sbar.map[m_old as usize];
let mut nuv: Vec<VertexId> = dom_subtree(&children, min_new)
.into_iter()
.map(|x| sbar.invmap[x as usize])
.collect();
let isv_min = restricted_bfs(graph, &gamma_s_vec, &nuv, n)?;
let blocked = isv_min.iter().any(|&u| t.iselement(u) || u == target);
if !blocked {
nuv.extend_from_slice(&leftout_old);
let isv = restricted_bfs(graph, &[m_old], &nuv, n)?;
return Ok((m_old, isv));
}
}
Ok((0, Vec::new()))
}
fn minimal_gamma(
dt: &DominatorTree,
sbar: &InducedSubgraphResult,
gamma_s: &[bool],
n: usize,
) -> Vec<VertexId> {
let mut nomark: Vec<bool> = (0..n).map(|i| !gamma_s[i]).collect();
for g_old in 0..n {
if !gamma_s[g_old] {
continue;
}
let gn = sbar.map[g_old];
if gn == u32::MAX {
continue;
}
let mut cur = dt.idom[gn as usize];
while cur >= 0 {
let anc_old = sbar.invmap[cur as usize] as usize;
if gamma_s[anc_old] {
nomark[anc_old] = true;
}
cur = dt.idom[cur as usize];
}
}
(0..n)
.filter(|&i| !nomark[i])
.map(|i| i as VertexId)
.collect()
}
fn dom_subtree(children: &[Vec<u32>], root: u32) -> Vec<u32> {
let mut out = Vec::new();
let mut stack = vec![root];
while let Some(v) = stack.pop() {
out.push(v);
for &c in &children[v as usize] {
stack.push(c);
}
}
out
}
fn restricted_bfs(
graph: &Graph,
roots: &[VertexId],
restricted: &[VertexId],
n: usize,
) -> IgraphResult<Vec<VertexId>> {
let mut allowed = vec![false; n];
for &r in restricted {
allowed[r as usize] = true;
}
let mut visited = vec![false; n];
let mut order: Vec<VertexId> = Vec::new();
let mut queue: std::collections::VecDeque<VertexId> = std::collections::VecDeque::new();
for &root in roots {
if !allowed[root as usize] || visited[root as usize] {
continue;
}
visited[root as usize] = true;
order.push(root);
queue.push_back(root);
while let Some(u) = queue.pop_front() {
for nei in graph.out_neighbors_vec(u)? {
if allowed[nei as usize] && !visited[nei as usize] {
visited[nei as usize] = true;
order.push(nei);
queue.push_back(nei);
}
}
}
}
Ok(order)
}
#[cfg(test)]
mod tests {
use super::*;
fn build(n: u32, edges: &[(u32, u32)]) -> Graph {
let mut g = Graph::new(n, true).expect("directed graph");
for &(u, v) in edges {
g.add_edge(u, v).expect("add edge");
}
g
}
fn target_reachable_minus_cut(
g: &Graph,
source: VertexId,
target: VertexId,
cut: &[EdgeId],
) -> bool {
let n = g.vcount() as usize;
let removed: std::collections::HashSet<EdgeId> = cut.iter().copied().collect();
let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
for e in 0..g.ecount() as EdgeId {
if removed.contains(&e) {
continue;
}
let (from, to) = g.edge(e).expect("edge");
adj[from as usize].push(to);
}
let mut visited = vec![false; n];
let mut stack = vec![source];
visited[source as usize] = true;
while let Some(u) = stack.pop() {
if u == target {
return true;
}
for &w in &adj[u as usize] {
if !visited[w as usize] {
visited[w as usize] = true;
stack.push(w);
}
}
}
false
}
fn assert_cuts_consistent(g: &Graph, source: VertexId, target: VertexId, res: &StCuts) {
assert_eq!(
res.cuts.len(),
res.partition1s.len(),
"cuts and partition1s must have equal length"
);
let n = g.vcount() as usize;
let mut seen: std::collections::HashSet<Vec<VertexId>> = std::collections::HashSet::new();
for (part, cut) in res.partition1s.iter().zip(res.cuts.iter()) {
for w in part.windows(2) {
assert!(w[0] < w[1], "partition not strictly ascending: {part:?}");
}
for w in cut.windows(2) {
assert!(w[0] < w[1], "cut not strictly ascending: {cut:?}");
}
assert!(
part.contains(&source),
"source {source} missing from {part:?}"
);
assert!(
!part.contains(&target),
"target {target} present in {part:?}"
);
assert!(seen.insert(part.clone()), "duplicate partition {part:?}");
let mut in_s = vec![false; n];
for &v in part {
in_s[v as usize] = true;
}
let mut crossing: Vec<EdgeId> = Vec::new();
for e in 0..g.ecount() as EdgeId {
let (from, to) = g.edge(e).expect("edge");
if in_s[from as usize] && !in_s[to as usize] {
crossing.push(e);
}
}
crossing.sort_unstable();
assert_eq!(cut, &crossing, "cut != crossing-out edges of {part:?}");
assert!(
!target_reachable_minus_cut(g, source, target, cut),
"target still reachable after removing cut {cut:?} (partition {part:?})"
);
}
}
#[test]
fn golden_six_node_nine_cuts() {
let g = build(6, &[(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (1, 5), (5, 4)]);
let res = all_st_cuts(&g, 0, 4).expect("compute");
let expected = StCuts {
partition1s: vec![
vec![0],
vec![0, 1],
vec![0, 1, 5],
vec![0, 1, 3],
vec![0, 1, 3, 5],
vec![0, 1, 2],
vec![0, 1, 2, 5],
vec![0, 1, 2, 3],
vec![0, 1, 2, 3, 5],
],
cuts: vec![
vec![0],
vec![1, 2, 5],
vec![1, 2, 6],
vec![1, 4, 5],
vec![1, 4, 6],
vec![2, 3, 5],
vec![2, 3, 6],
vec![3, 4, 5],
vec![3, 4, 6],
],
};
assert_eq!(res, expected, "golden fixture mismatch");
assert_cuts_consistent(&g, 0, 4, &res);
}
#[test]
fn simple_path_two_cuts() {
let g = build(3, &[(0, 1), (1, 2)]);
let res = all_st_cuts(&g, 0, 2).expect("compute");
assert_eq!(res.partition1s, vec![vec![0], vec![0, 1]]);
assert_eq!(res.cuts, vec![vec![0], vec![1]]);
assert_cuts_consistent(&g, 0, 2, &res);
}
#[test]
fn complete_digraph_k5_has_eight_cuts() {
let mut edges = Vec::new();
for u in 0..5 {
for v in 0..5 {
if u != v {
edges.push((u, v));
}
}
}
let g = build(5, &edges);
let res = all_st_cuts(&g, 0, 4).expect("compute");
assert_eq!(res.cuts.len(), 8, "K5 should yield 2^(5-2) = 8 cuts");
assert_cuts_consistent(&g, 0, 4, &res);
}
#[test]
fn empty_graph_source_out_of_range() {
let g = Graph::new(0, true).expect("empty directed graph");
let err = all_st_cuts(&g, 0, 1).expect_err("must reject empty graph");
match err {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!(id, 0);
assert_eq!(n, 0);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn single_vertex_rejects_equal_endpoints() {
let g = Graph::new(1, true).expect("single vertex directed graph");
let err = all_st_cuts(&g, 0, 0).expect_err("must reject source == target");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
let err2 = all_st_cuts(&g, 0, 1).expect_err("must reject out-of-range target");
assert!(matches!(
err2,
IgraphError::VertexOutOfRange { id: 1, n: 1 }
));
}
#[test]
fn rejects_undirected_graph() {
let g = Graph::with_vertices(4);
let err = all_st_cuts(&g, 0, 3).expect_err("must reject undirected");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn rejects_equal_source_and_target() {
let g = build(3, &[(0, 1), (1, 2)]);
let err = all_st_cuts(&g, 1, 1).expect_err("must reject source == target");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn rejects_out_of_range_endpoints() {
let g = build(3, &[(0, 1), (1, 2)]);
match all_st_cuts(&g, 5, 1).expect_err("bad source") {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!((id, n), (5, 3));
}
other => panic!("unexpected error: {other:?}"),
}
match all_st_cuts(&g, 0, 9).expect_err("bad target") {
IgraphError::VertexOutOfRange { id, n } => {
assert_eq!((id, n), (9, 3));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn unreachable_target_is_consistent() {
let g = build(3, &[(1, 2)]);
let res = all_st_cuts(&g, 0, 2).expect("compute");
assert!(res.cuts.is_empty(), "expected empty cut list, got {res:?}");
assert_cuts_consistent(&g, 0, 2, &res);
}
}