use std::collections::VecDeque;
use crate::algorithms::connectivity::articulation::articulation_points;
use crate::algorithms::flow::all_st_mincuts::all_st_mincuts;
use crate::algorithms::flow::max_flow::max_flow_value;
use crate::algorithms::flow::vertex_connectivity::vertex_connectivity;
use crate::algorithms::operators::even_tarjan::even_tarjan_reduction;
use crate::algorithms::operators::simplify::simplify;
use crate::algorithms::properties::are_adjacent::are_adjacent;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn is_separator(graph: &Graph, candidates: &[VertexId]) -> IgraphResult<bool> {
if graph.is_directed() {
return Err(IgraphError::InvalidArgument(
"is_separator: only defined for undirected graphs".into(),
));
}
let n = graph.vcount();
for &v in candidates {
if v >= n {
return Err(IgraphError::InvalidArgument(format!(
"is_separator: vertex {v} out of range (vcount={n})"
)));
}
}
if n == 0 {
return Ok(false);
}
let n_us = n as usize;
let mut removed = vec![false; n_us];
for &v in candidates {
removed[v as usize] = true;
}
let remaining = (0..n_us).filter(|&v| !removed[v]).count();
if remaining == 0 {
return Ok(false);
}
let Some(start) = (0..n_us).find(|&v| !removed[v]) else {
return Ok(false);
};
#[allow(clippy::cast_possible_truncation)] let reached = bfs_count(graph, start as u32, &removed)?;
Ok(reached < remaining)
}
pub fn is_minimal_separator(graph: &Graph, candidates: &[VertexId]) -> IgraphResult<bool> {
if graph.is_directed() {
return Err(IgraphError::InvalidArgument(
"is_minimal_separator: only defined for undirected graphs".into(),
));
}
let n = graph.vcount();
for &v in candidates {
if v >= n {
return Err(IgraphError::InvalidArgument(format!(
"is_minimal_separator: vertex {v} out of range (vcount={n})"
)));
}
}
if !is_separator(graph, candidates)? {
return Ok(false);
}
let n_us = n as usize;
for (idx, _) in candidates.iter().enumerate() {
let mut removed = vec![false; n_us];
for (j, &u) in candidates.iter().enumerate() {
if j != idx {
removed[u as usize] = true;
}
}
let remaining = (0..n_us).filter(|&x| !removed[x]).count();
if remaining == 0 {
continue;
}
let Some(start) = (0..n_us).find(|&x| !removed[x]) else {
continue;
};
#[allow(clippy::cast_possible_truncation)] let reached = bfs_count(graph, start as u32, &removed)?;
if reached < remaining {
return Ok(false);
}
}
Ok(true)
}
fn bfs_count(graph: &Graph, start: u32, removed: &[bool]) -> IgraphResult<usize> {
let n_us = graph.vcount() as usize;
let mut visited = vec![false; n_us];
let mut queue = VecDeque::new();
let mut count = 0usize;
visited[start as usize] = true;
queue.push_back(start);
count += 1;
while let Some(cur) = queue.pop_front() {
let neighbors = graph.neighbors(cur)?;
for &nb in &neighbors {
let nidx = nb as usize;
if !visited[nidx] && !removed[nidx] {
visited[nidx] = true;
queue.push_back(nb);
count += 1;
}
}
}
Ok(count)
}
pub fn all_minimal_st_separators(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
let n = graph.vcount() as usize;
if n == 0 {
return Ok(Vec::new());
}
let adj = build_adj_undirected(graph)?;
let mut separators: Vec<Vec<VertexId>> = Vec::new();
let mut mark: Vec<u32> = vec![0; n];
let mut stamp: u32 = 1;
for v in 0..n {
advance_stamp(&mut mark, &mut stamp, n);
mark[v] = stamp;
for &nb in &adj[v] {
mark[nb as usize] = stamp;
}
let components = find_components_leaveout(&adj, &mark, stamp, n);
store_separators(&adj, &components, &mut mark, &mut separators, &mut stamp, n);
}
let mut try_next = 0;
while try_next < separators.len() {
let basis = separators[try_next].clone();
for &x in &basis {
advance_stamp(&mut mark, &mut stamp, n);
for &sv in &basis {
mark[sv as usize] = stamp;
}
for &nb in &adj[x as usize] {
mark[nb as usize] = stamp;
}
let components = find_components_leaveout(&adj, &mark, stamp, n);
store_separators(&adj, &components, &mut mark, &mut separators, &mut stamp, n);
}
try_next += 1;
}
Ok(separators)
}
fn build_adj_undirected(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
let n = graph.vcount() as usize;
let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
let m = graph.ecount();
for eid in 0..m {
let eid32 = u32::try_from(eid).map_err(|_| {
IgraphError::InvalidArgument("all_minimal_st_separators: edge id overflow".into())
})?;
let (from, to) = graph.edge(eid32)?;
adj[from as usize].push(to);
if from != to {
adj[to as usize].push(from);
}
}
Ok(adj)
}
fn find_components_leaveout(
adj: &[Vec<VertexId>],
mark: &[u32],
stamp: u32,
n: usize,
) -> Vec<Vec<VertexId>> {
let mut visited = vec![false; n];
let mut components: Vec<Vec<VertexId>> = Vec::new();
let mut queue: VecDeque<VertexId> = VecDeque::new();
for i in 0..n {
if mark[i] == stamp || visited[i] {
continue;
}
let mut comp: Vec<VertexId> = Vec::new();
#[allow(clippy::cast_possible_truncation)]
let i_v = i as VertexId;
visited[i] = true;
queue.push_back(i_v);
comp.push(i_v);
while let Some(cur) = queue.pop_front() {
for &nb in &adj[cur as usize] {
let nb_us = nb as usize;
if mark[nb_us] == stamp || visited[nb_us] {
continue;
}
visited[nb_us] = true;
queue.push_back(nb);
comp.push(nb);
}
}
components.push(comp);
}
components
}
fn store_separators(
adj: &[Vec<VertexId>],
components: &[Vec<VertexId>],
mark: &mut [u32],
separators: &mut Vec<Vec<VertexId>>,
cur_stamp: &mut u32,
n: usize,
) {
for comp in components {
advance_stamp(mark, cur_stamp, n);
let comp_stamp = *cur_stamp;
for &v in comp {
mark[v as usize] = comp_stamp;
}
let mut neighborhood: Vec<VertexId> = Vec::new();
for &v in comp {
for &nb in &adj[v as usize] {
let nb_us = nb as usize;
if mark[nb_us] != comp_stamp {
mark[nb_us] = comp_stamp;
neighborhood.push(nb);
}
}
}
if neighborhood.is_empty() {
continue;
}
neighborhood.sort_unstable();
if !separators.contains(&neighborhood) {
separators.push(neighborhood);
}
}
advance_stamp(mark, cur_stamp, n);
}
fn advance_stamp(mark: &mut [u32], stamp: &mut u32, _n: usize) {
*stamp = stamp.wrapping_add(1);
if *stamp == 0 {
for m in mark.iter_mut() {
*m = 0;
}
*stamp = 1;
}
}
fn topk_degree(graph: &Graph, k: usize) -> IgraphResult<Vec<VertexId>> {
let n = graph.vcount();
let mut deg: Vec<(usize, VertexId)> = Vec::with_capacity(n as usize);
for v in 0..n {
deg.push((graph.degree(v)?, v));
}
deg.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
let mut res = Vec::with_capacity(k);
for i in 0..k {
res.push(deg[n as usize - 1 - i].1);
}
Ok(res)
}
fn append_unique(acc: &mut Vec<Vec<VertexId>>, new: Vec<Vec<VertexId>>) {
for sep in new {
if !acc.iter().any(|existing| existing == &sep) {
acc.push(sep);
}
}
}
#[allow(clippy::too_many_lines)]
pub fn minimum_size_separators(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
if graph.is_directed() {
return Err(IgraphError::InvalidArgument(
"minimum_size_separators: only defined for undirected graphs".into(),
));
}
let no_of_nodes = graph.vcount();
let conn = vertex_connectivity(graph, true)?;
if conn <= 0 {
return Ok(Vec::new());
}
if conn == 1 {
let aps = articulation_points(graph)?;
return Ok(aps.into_iter().map(|v| vec![v]).collect());
}
if conn == i64::from(no_of_nodes) - 1 {
let mut separators = Vec::with_capacity(no_of_nodes as usize);
for i in 0..no_of_nodes {
separators.push((0..no_of_nodes).filter(|&j| j != i).collect());
}
return Ok(separators);
}
let k = usize::try_from(conn).map_err(|_| {
IgraphError::InvalidArgument("minimum_size_separators: connectivity overflow".into())
})?;
let mut graph_copy = simplify(graph, true, true)?;
let mut separators: Vec<Vec<VertexId>> = Vec::new();
let x = topk_degree(&graph_copy, k)?;
if is_separator(&graph_copy, &x)? {
let mut sep = x.clone();
sep.sort_unstable();
append_unique(&mut separators, vec![sep]);
}
let reduction = even_tarjan_reduction(&graph_copy)?;
let mut gbar = reduction.graph;
let big = f64::from(no_of_nodes);
let mut capacity: Vec<f64> = reduction
.capacity
.into_iter()
.map(|c| if c.is_finite() { c } else { big })
.collect();
let k_f = f64::from(u32::try_from(conn).map_err(|_| {
IgraphError::InvalidArgument("minimum_size_separators: connectivity overflow".into())
})?);
for &xi in &x {
for j in 0..no_of_nodes {
if xi == j {
continue;
}
if are_adjacent(&graph_copy, xi, j)? {
continue;
}
let source = xi + no_of_nodes;
let target = j;
let phivalue = max_flow_value(&gbar, source, target, Some(&capacity))?;
if (phivalue - k_f).abs() < 0.5 {
let cuts = all_st_mincuts(&gbar, source, target, Some(&capacity))?;
let mapped: Vec<Vec<VertexId>> = cuts
.cuts
.into_iter()
.map(|cut| {
let mut sep: Vec<VertexId> = cut;
sep.sort_unstable();
sep
})
.collect();
append_unique(&mut separators, mapped);
}
graph_copy.add_edge(xi, j)?;
gbar.add_edge(xi + no_of_nodes, j)?;
gbar.add_edge(j + no_of_nodes, xi)?;
capacity.push(big);
capacity.push(big);
}
}
Ok(separators)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph() {
let g = Graph::with_vertices(0);
assert!(!is_separator(&g, &[]).unwrap());
}
#[test]
fn singleton_not_separator() {
let g = Graph::with_vertices(1);
assert!(!is_separator(&g, &[0]).unwrap());
}
#[test]
fn path_middle_is_separator() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(is_separator(&g, &[1]).unwrap());
}
#[test]
fn path_leaf_not_separator() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(!is_separator(&g, &[0]).unwrap());
assert!(!is_separator(&g, &[2]).unwrap());
}
#[test]
fn triangle_no_single_separator() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert!(!is_separator(&g, &[0]).unwrap());
assert!(!is_separator(&g, &[1]).unwrap());
assert!(!is_separator(&g, &[2]).unwrap());
}
#[test]
fn triangle_pair_not_separator() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert!(!is_separator(&g, &[0, 1]).unwrap());
}
#[test]
fn cycle_4_opposite_vertices() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
assert!(is_separator(&g, &[1, 3]).unwrap());
}
#[test]
fn cycle_4_adjacent_not_separator() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
assert!(!is_separator(&g, &[0, 1]).unwrap());
}
#[test]
fn already_disconnected_empty_set_is_separator() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
assert!(is_separator(&g, &[]).unwrap());
}
#[test]
fn k4_articulation() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
assert!(!is_separator(&g, &[0]).unwrap());
assert!(!is_separator(&g, &[2]).unwrap());
}
#[test]
fn bowtie_articulation() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 2).unwrap();
assert!(is_separator(&g, &[2]).unwrap());
}
#[test]
fn directed_rejected() {
let g = Graph::new(3, true).unwrap();
assert!(is_separator(&g, &[0]).is_err());
assert!(is_minimal_separator(&g, &[0]).is_err());
}
#[test]
fn out_of_range_rejected() {
let g = Graph::with_vertices(3);
assert!(is_separator(&g, &[5]).is_err());
}
#[test]
fn minimal_path_middle() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(is_minimal_separator(&g, &[1]).unwrap());
}
#[test]
fn minimal_cycle_4_opposite() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
assert!(is_minimal_separator(&g, &[1, 3]).unwrap());
}
#[test]
fn not_minimal_superset() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
assert!(is_separator(&g, &[1, 3]).unwrap());
assert!(!is_minimal_separator(&g, &[1, 3]).unwrap());
}
#[test]
fn not_separator_not_minimal() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert!(!is_minimal_separator(&g, &[0]).unwrap());
}
#[test]
fn minimal_bowtie_articulation() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 2).unwrap();
assert!(is_minimal_separator(&g, &[2]).unwrap());
}
#[test]
fn all_min_sep_empty_graph() {
let g = Graph::with_vertices(0);
let seps = all_minimal_st_separators(&g).unwrap();
assert!(seps.is_empty());
}
#[test]
fn all_min_sep_single_vertex() {
let g = Graph::with_vertices(1);
let seps = all_minimal_st_separators(&g).unwrap();
assert!(seps.is_empty());
}
#[test]
fn all_min_sep_single_edge() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert!(seps.is_empty());
}
#[test]
fn all_min_sep_path_3() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert_eq!(seps.len(), 1);
assert_eq!(seps[0], vec![1]);
}
#[test]
fn all_min_sep_path_5() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert_eq!(seps.len(), 3);
assert!(seps.contains(&vec![1]));
assert!(seps.contains(&vec![2]));
assert!(seps.contains(&vec![3]));
}
#[test]
fn all_min_sep_cycle_4() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert_eq!(seps.len(), 2);
assert!(seps.contains(&vec![0, 2]));
assert!(seps.contains(&vec![1, 3]));
}
#[test]
fn all_min_sep_pentagon_with_chord() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 1).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert_eq!(seps.len(), 3);
assert!(seps.contains(&vec![1]));
assert!(seps.contains(&vec![2, 4]));
assert!(seps.contains(&vec![1, 3]));
}
#[test]
fn all_min_sep_bowtie() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 2).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert_eq!(seps.len(), 1);
assert_eq!(seps[0], vec![2]);
}
#[test]
fn all_min_sep_complete_graph() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
assert!(seps.is_empty());
}
#[test]
fn all_min_sep_disconnected() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
let seps = all_minimal_st_separators(&g).unwrap();
for s in &seps {
assert!(!s.is_empty());
}
}
fn canon(mut seps: Vec<Vec<VertexId>>) -> Vec<Vec<VertexId>> {
for s in &mut seps {
s.sort_unstable();
}
seps.sort();
seps
}
fn undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
let mut g = Graph::with_vertices(n);
for &(a, b) in edges {
g.add_edge(a, b).unwrap();
}
g
}
#[test]
fn mss_directed_rejected() {
let g = Graph::new(3, true).unwrap();
assert!(minimum_size_separators(&g).is_err());
}
#[test]
fn mss_path3() {
let g = undirected(3, &[(0, 1), (1, 2)]);
assert_eq!(canon(minimum_size_separators(&g).unwrap()), vec![vec![1]]);
}
#[test]
fn mss_disconnected_empty() {
let g = undirected(4, &[(0, 1), (2, 3)]);
assert!(minimum_size_separators(&g).unwrap().is_empty());
}
#[test]
fn mss_articulation_singletons() {
let g = undirected(5, &[(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (2, 4)]);
assert_eq!(canon(minimum_size_separators(&g).unwrap()), vec![vec![2]]);
}
#[test]
fn mss_complete_k4() {
let g = undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
assert_eq!(
canon(minimum_size_separators(&g).unwrap()),
vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 2, 3], vec![1, 2, 3],]
);
}
#[test]
fn mss_complete_k3() {
let g = undirected(3, &[(0, 1), (0, 2), (1, 2)]);
assert_eq!(
canon(minimum_size_separators(&g).unwrap()),
vec![vec![0, 1], vec![0, 2], vec![1, 2]]
);
}
#[test]
fn mss_cycle5() {
let g = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
assert_eq!(
canon(minimum_size_separators(&g).unwrap()),
vec![vec![0, 2], vec![0, 3], vec![1, 3], vec![1, 4], vec![2, 4],]
);
}
#[test]
fn mss_grid3x3() {
let g = undirected(
9,
&[
(0, 1),
(1, 2),
(3, 4),
(4, 5),
(6, 7),
(7, 8),
(0, 3),
(3, 6),
(1, 4),
(4, 7),
(2, 5),
(5, 8),
],
);
assert_eq!(
canon(minimum_size_separators(&g).unwrap()),
vec![vec![1, 3], vec![1, 5], vec![3, 7], vec![5, 7]]
);
}
#[test]
fn mss_complete_bipartite_k23() {
let g = undirected(5, &[(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)]);
assert_eq!(
canon(minimum_size_separators(&g).unwrap()),
vec![vec![0, 1]]
);
}
#[test]
fn mss_separators_are_valid() {
let g = undirected(
7,
&[
(0, 1),
(1, 2),
(2, 0),
(2, 3),
(3, 4),
(4, 5),
(5, 3),
(5, 6),
(6, 3),
],
);
let seps = minimum_size_separators(&g).unwrap();
assert!(!seps.is_empty());
let k = seps[0].len();
for s in &seps {
assert_eq!(s.len(), k, "all minimum separators share size");
assert!(is_separator(&g, s).unwrap(), "{s:?} must separate");
}
}
}