Skip to main content

rust_igraph/algorithms/connectivity/
separators.rs

1//! Vertex separators (ALGO-CN-015).
2//!
3//! Counterpart of `igraph_is_separator()` and
4//! `igraph_is_minimal_separator()` from
5//! `references/igraph/src/connectivity/separators.c`.
6//!
7//! A *vertex separator* of a connected graph is a set of vertices
8//! whose removal disconnects the graph (or isolates a vertex from
9//! the rest). A separator is *minimal* if no proper subset of it is
10//! also a separator.
11
12use std::collections::VecDeque;
13
14use crate::algorithms::connectivity::articulation::articulation_points;
15use crate::algorithms::flow::all_st_mincuts::all_st_mincuts;
16use crate::algorithms::flow::max_flow::max_flow_value;
17use crate::algorithms::flow::vertex_connectivity::vertex_connectivity;
18use crate::algorithms::operators::even_tarjan::even_tarjan_reduction;
19use crate::algorithms::operators::simplify::simplify;
20use crate::algorithms::properties::are_adjacent::are_adjacent;
21use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
22
23/// Check whether a set of vertices is a separator of the graph.
24///
25/// A vertex set S is a separator if removing S (and all incident edges)
26/// makes the remaining graph disconnected, OR if removing S leaves
27/// fewer vertices than the original graph minus |S| (i.e., some vertex
28/// becomes isolated). For a graph that is already disconnected, any
29/// set is technically a separator — this function returns `true` for
30/// the empty set in that case.
31///
32/// For undirected graphs only.
33///
34/// # Errors
35///
36/// - `InvalidArgument` if the graph is directed.
37/// - `InvalidArgument` if any vertex ID in `candidates` is out of range.
38///
39/// # Examples
40///
41/// ```
42/// use rust_igraph::{Graph, is_separator};
43///
44/// // Path 0-1-2: removing vertex 1 disconnects the graph.
45/// let mut g = Graph::with_vertices(3);
46/// g.add_edge(0, 1).unwrap();
47/// g.add_edge(1, 2).unwrap();
48/// assert!(is_separator(&g, &[1]).unwrap());
49/// assert!(!is_separator(&g, &[0]).unwrap()); // leaf removal doesn't disconnect
50/// ```
51pub fn is_separator(graph: &Graph, candidates: &[VertexId]) -> IgraphResult<bool> {
52    if graph.is_directed() {
53        return Err(IgraphError::InvalidArgument(
54            "is_separator: only defined for undirected graphs".into(),
55        ));
56    }
57
58    let n = graph.vcount();
59    for &v in candidates {
60        if v >= n {
61            return Err(IgraphError::InvalidArgument(format!(
62                "is_separator: vertex {v} out of range (vcount={n})"
63            )));
64        }
65    }
66
67    if n == 0 {
68        return Ok(false);
69    }
70
71    // Mark candidates in a set for O(1) lookup.
72    let n_us = n as usize;
73    let mut removed = vec![false; n_us];
74    for &v in candidates {
75        removed[v as usize] = true;
76    }
77
78    // Count remaining vertices (those not in the removed set).
79    let remaining = (0..n_us).filter(|&v| !removed[v]).count();
80
81    if remaining == 0 {
82        return Ok(false);
83    }
84
85    // BFS from the first non-removed vertex. If it can't reach all
86    // remaining vertices, the graph is disconnected → separator.
87    let start = (0..n_us).find(|&v| !removed[v]).unwrap();
88    #[allow(clippy::cast_possible_truncation)] // start < n which is u32
89    let reached = bfs_count(graph, start as u32, &removed)?;
90
91    Ok(reached < remaining)
92}
93
94/// Check whether a set of vertices is a *minimal* separator.
95///
96/// A separator is minimal if no proper subset is also a separator.
97/// Equivalently, S is a minimal separator if it is a separator and
98/// for every vertex v in S, removing S \ {v} does NOT disconnect the
99/// graph.
100///
101/// # Errors
102///
103/// - `InvalidArgument` if the graph is directed.
104/// - `InvalidArgument` if any vertex ID is out of range.
105///
106/// # Examples
107///
108/// ```
109/// use rust_igraph::{Graph, is_minimal_separator};
110///
111/// // 4-cycle: {1,3} is a minimal separator (removing both disconnects 0 from 2).
112/// let mut g = Graph::with_vertices(4);
113/// g.add_edge(0, 1).unwrap();
114/// g.add_edge(1, 2).unwrap();
115/// g.add_edge(2, 3).unwrap();
116/// g.add_edge(3, 0).unwrap();
117/// assert!(is_minimal_separator(&g, &[1, 3]).unwrap());
118/// // {0,1,3} leaves only vertex 2 — not a separator, hence not minimal.
119/// assert!(!is_minimal_separator(&g, &[0, 1, 3]).unwrap());
120/// ```
121pub fn is_minimal_separator(graph: &Graph, candidates: &[VertexId]) -> IgraphResult<bool> {
122    if graph.is_directed() {
123        return Err(IgraphError::InvalidArgument(
124            "is_minimal_separator: only defined for undirected graphs".into(),
125        ));
126    }
127
128    let n = graph.vcount();
129    for &v in candidates {
130        if v >= n {
131            return Err(IgraphError::InvalidArgument(format!(
132                "is_minimal_separator: vertex {v} out of range (vcount={n})"
133            )));
134        }
135    }
136
137    // First check: is it a separator at all?
138    if !is_separator(graph, candidates)? {
139        return Ok(false);
140    }
141
142    // For each vertex in the candidate set, check if removing it
143    // still leaves a separator. If yes for any vertex, it's not minimal.
144    let n_us = n as usize;
145    for (idx, _) in candidates.iter().enumerate() {
146        // Build the "removed" set without vertex v.
147        let mut removed = vec![false; n_us];
148        for (j, &u) in candidates.iter().enumerate() {
149            if j != idx {
150                removed[u as usize] = true;
151            }
152        }
153
154        let remaining = (0..n_us).filter(|&x| !removed[x]).count();
155        if remaining == 0 {
156            continue;
157        }
158
159        let start = (0..n_us).find(|&x| !removed[x]).unwrap();
160        #[allow(clippy::cast_possible_truncation)] // start < n which is u32
161        let reached = bfs_count(graph, start as u32, &removed)?;
162
163        if reached < remaining {
164            // S \ {v} is still a separator → S is not minimal.
165            return Ok(false);
166        }
167    }
168
169    Ok(true)
170}
171
172/// BFS from `start`, skipping removed vertices. Returns count of reachable vertices.
173fn bfs_count(graph: &Graph, start: u32, removed: &[bool]) -> IgraphResult<usize> {
174    let n_us = graph.vcount() as usize;
175    let mut visited = vec![false; n_us];
176    let mut queue = VecDeque::new();
177    let mut count = 0usize;
178
179    visited[start as usize] = true;
180    queue.push_back(start);
181    count += 1;
182
183    while let Some(cur) = queue.pop_front() {
184        let neighbors = graph.neighbors(cur)?;
185        for &nb in &neighbors {
186            let nidx = nb as usize;
187            if !visited[nidx] && !removed[nidx] {
188                visited[nidx] = true;
189                queue.push_back(nb);
190                count += 1;
191            }
192        }
193    }
194
195    Ok(count)
196}
197
198/// List all vertex sets that are minimal (s,t) separators for some s and t.
199///
200/// A vertex set S is a *minimal (s,t) separator* if removing S disconnects
201/// s from t, and no proper subset of S does the same for that pair.
202///
203/// This function enumerates ALL such sets (for all possible pairs s,t).
204/// Note that a returned separator may not be minimal with respect to
205/// *disconnecting the graph* — see the igraph docs for details.
206///
207/// Based on Berry, Bordat & Cogis (1999): "Generating All the Minimal
208/// Separators of a Graph".
209///
210/// Edge directions are ignored (the graph is treated as undirected).
211///
212/// # Examples
213///
214/// ```
215/// use rust_igraph::{Graph, all_minimal_st_separators};
216///
217/// // Path 0-1-2-3-4-1 (pentagon with chord):
218/// // edges: 0-1, 1-2, 2-3, 3-4, 4-1
219/// let mut g = Graph::with_vertices(5);
220/// g.add_edge(0, 1).unwrap();
221/// g.add_edge(1, 2).unwrap();
222/// g.add_edge(2, 3).unwrap();
223/// g.add_edge(3, 4).unwrap();
224/// g.add_edge(4, 1).unwrap();
225/// let seps = all_minimal_st_separators(&g).unwrap();
226/// // Should contain {1}, {2,4}, {1,3}
227/// assert!(seps.iter().any(|s| s == &[1]));
228/// assert!(seps.iter().any(|s| s == &[2, 4]));
229/// assert!(seps.iter().any(|s| s == &[1, 3]));
230/// ```
231pub fn all_minimal_st_separators(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
232    let n = graph.vcount() as usize;
233    if n == 0 {
234        return Ok(Vec::new());
235    }
236
237    let adj = build_adj_undirected(graph)?;
238
239    let mut separators: Vec<Vec<VertexId>> = Vec::new();
240    let mut mark: Vec<u32> = vec![0; n];
241    let mut stamp: u32 = 1;
242
243    // Phase 1 (Initialization): For each vertex v, mark N[v] as removed,
244    // find components in remaining graph, compute N(C) for each component.
245    for v in 0..n {
246        advance_stamp(&mut mark, &mut stamp, n);
247        mark[v] = stamp;
248        for &nb in &adj[v] {
249            mark[nb as usize] = stamp;
250        }
251
252        let components = find_components_leaveout(&adj, &mark, stamp, n);
253        store_separators(&adj, &components, &mut mark, &mut separators, &mut stamp, n);
254    }
255
256    // Phase 2 (Generation): Use found separators as basis to find more.
257    let mut try_next = 0;
258    while try_next < separators.len() {
259        let basis = separators[try_next].clone();
260        for &x in &basis {
261            advance_stamp(&mut mark, &mut stamp, n);
262            for &sv in &basis {
263                mark[sv as usize] = stamp;
264            }
265            for &nb in &adj[x as usize] {
266                mark[nb as usize] = stamp;
267            }
268
269            let components = find_components_leaveout(&adj, &mark, stamp, n);
270            store_separators(&adj, &components, &mut mark, &mut separators, &mut stamp, n);
271        }
272        try_next += 1;
273    }
274
275    Ok(separators)
276}
277
278fn build_adj_undirected(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
279    let n = graph.vcount() as usize;
280    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
281    let m = graph.ecount();
282    for eid in 0..m {
283        let eid32 = u32::try_from(eid).map_err(|_| {
284            IgraphError::InvalidArgument("all_minimal_st_separators: edge id overflow".into())
285        })?;
286        let (from, to) = graph.edge(eid32)?;
287        adj[from as usize].push(to);
288        if from != to {
289            adj[to as usize].push(from);
290        }
291    }
292    Ok(adj)
293}
294
295/// Find connected components among vertices not marked with `stamp`.
296/// Returns a list of components; each component is a Vec of vertex ids.
297fn find_components_leaveout(
298    adj: &[Vec<VertexId>],
299    mark: &[u32],
300    stamp: u32,
301    n: usize,
302) -> Vec<Vec<VertexId>> {
303    let mut visited = vec![false; n];
304    let mut components: Vec<Vec<VertexId>> = Vec::new();
305    let mut queue: VecDeque<VertexId> = VecDeque::new();
306
307    for i in 0..n {
308        if mark[i] == stamp || visited[i] {
309            continue;
310        }
311
312        let mut comp: Vec<VertexId> = Vec::new();
313        #[allow(clippy::cast_possible_truncation)]
314        let i_v = i as VertexId;
315        visited[i] = true;
316        queue.push_back(i_v);
317        comp.push(i_v);
318
319        while let Some(cur) = queue.pop_front() {
320            for &nb in &adj[cur as usize] {
321                let nb_us = nb as usize;
322                if mark[nb_us] == stamp || visited[nb_us] {
323                    continue;
324                }
325                visited[nb_us] = true;
326                queue.push_back(nb);
327                comp.push(nb);
328            }
329        }
330
331        components.push(comp);
332    }
333
334    components
335}
336
337/// For each component C, compute N(C) = vertices adjacent to C but not in C.
338/// Since C is a connected component of G - S, N(C) ⊆ S. Store as a new
339/// separator if not already seen. Advances `cur_stamp` afterward.
340fn store_separators(
341    adj: &[Vec<VertexId>],
342    components: &[Vec<VertexId>],
343    mark: &mut [u32],
344    separators: &mut Vec<Vec<VertexId>>,
345    cur_stamp: &mut u32,
346    n: usize,
347) {
348    for comp in components {
349        advance_stamp(mark, cur_stamp, n);
350        let comp_stamp = *cur_stamp;
351
352        // Mark component vertices
353        for &v in comp {
354            mark[v as usize] = comp_stamp;
355        }
356
357        // Collect neighbors not in C (they must be in the separator S)
358        let mut neighborhood: Vec<VertexId> = Vec::new();
359        for &v in comp {
360            for &nb in &adj[v as usize] {
361                let nb_us = nb as usize;
362                if mark[nb_us] != comp_stamp {
363                    mark[nb_us] = comp_stamp;
364                    neighborhood.push(nb);
365                }
366            }
367        }
368
369        if neighborhood.is_empty() {
370            continue;
371        }
372
373        neighborhood.sort_unstable();
374
375        if !separators.contains(&neighborhood) {
376            separators.push(neighborhood);
377        }
378    }
379
380    advance_stamp(mark, cur_stamp, n);
381}
382
383fn advance_stamp(mark: &mut [u32], stamp: &mut u32, _n: usize) {
384    *stamp = stamp.wrapping_add(1);
385    if *stamp == 0 {
386        for m in mark.iter_mut() {
387            *m = 0;
388        }
389        *stamp = 1;
390    }
391}
392
393/// Find the `k` vertices of largest degree (ties broken by descending
394/// vertex id, matching igraph's `igraph_i_vector_int_order` convention:
395/// a stable ascending sort whose tail is read back). The graph is
396/// assumed simple.
397fn topk_degree(graph: &Graph, k: usize) -> IgraphResult<Vec<VertexId>> {
398    let n = graph.vcount();
399    let mut deg: Vec<(usize, VertexId)> = Vec::with_capacity(n as usize);
400    for v in 0..n {
401        deg.push((graph.degree(v)?, v));
402    }
403    // Stable ascending order by (degree, id); the k highest-degree
404    // vertices are the last k. Reading them back high-to-low yields the
405    // higher-id member first among equal degrees.
406    deg.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
407    let mut res = Vec::with_capacity(k);
408    for i in 0..k {
409        res.push(deg[n as usize - 1 - i].1);
410    }
411    Ok(res)
412}
413
414/// Append every separator in `new` to `acc` that is not already present.
415/// Each separator is compared as an ordered vector (igraph
416/// `igraph_vector_int_all_e` semantics); callers pass canonically
417/// (ascending) sorted vectors so set-equality reduces to vector-equality.
418fn append_unique(acc: &mut Vec<Vec<VertexId>>, new: Vec<Vec<VertexId>>) {
419    for sep in new {
420        if !acc.iter().any(|existing| existing == &sep) {
421            acc.push(sep);
422        }
423    }
424}
425
426/// Find all minimum-size separating vertex sets.
427///
428/// A vertex set is a *separator* if its removal disconnects the graph.
429/// This function lists every separator of minimum cardinality (the
430/// minimum equals the graph's vertex connectivity).
431///
432/// Conventions, matching `igraph_minimum_size_separators`:
433///
434/// * If the graph is already disconnected, no separators are returned
435///   (this differs from [`all_minimal_st_separators`]).
436/// * Complete graphs have no vertex separators, so the result is empty.
437/// * For a graph whose connectivity is `1`, the minimum separators are
438///   exactly the articulation points (each returned as a singleton).
439///
440/// Each returned separator is sorted ascending, and the list contains no
441/// duplicates. The separators themselves are returned in an arbitrary
442/// order.
443///
444/// The implementation follows Arkady Kanevsky, "Finding all minimum-size
445/// separating vertex sets in a graph", Networks 23 (1993), 533–541. It
446/// computes the vertex connectivity `k`, takes the `k` largest-degree
447/// vertices `X`, and for each `x ∈ X` and each non-adjacent vertex `j`
448/// computes a maximum flow on the Even–Tarjan reduction; whenever the
449/// flow equals `k` it enumerates all minimum `(x, j)` vertex cuts via
450/// [`all_st_mincuts`], deduplicating as it goes. After each `(x, j)`
451/// pair an edge `(x, j)` is added so subsequent flows discover only new
452/// separators.
453///
454/// For undirected graphs only.
455///
456/// # Errors
457///
458/// - [`IgraphError::InvalidArgument`] if the graph is directed.
459///
460/// # Examples
461///
462/// ```
463/// use rust_igraph::{Graph, minimum_size_separators};
464///
465/// // Path 0-1-2: vertex 1 is the unique minimum separator.
466/// let mut g = Graph::with_vertices(3);
467/// g.add_edge(0, 1).unwrap();
468/// g.add_edge(1, 2).unwrap();
469/// let seps = minimum_size_separators(&g).unwrap();
470/// assert_eq!(seps, vec![vec![1]]);
471/// ```
472#[allow(clippy::too_many_lines)]
473pub fn minimum_size_separators(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
474    if graph.is_directed() {
475        return Err(IgraphError::InvalidArgument(
476            "minimum_size_separators: only defined for undirected graphs".into(),
477        ));
478    }
479
480    let no_of_nodes = graph.vcount();
481
482    // Step 1: vertex connectivity k.
483    let conn = vertex_connectivity(graph, true)?;
484
485    // Special cases (three early exits). Disconnected graphs have no
486    // separators; connectivity 1 ⇒ articulation points; a complete graph
487    // (connectivity n-1) has every (n-1)-subset of vertices as a
488    // minimum separator.
489    if conn <= 0 {
490        return Ok(Vec::new());
491    }
492    if conn == 1 {
493        let aps = articulation_points(graph)?;
494        return Ok(aps.into_iter().map(|v| vec![v]).collect());
495    }
496    if conn == i64::from(no_of_nodes) - 1 {
497        let mut separators = Vec::with_capacity(no_of_nodes as usize);
498        for i in 0..no_of_nodes {
499            separators.push((0..no_of_nodes).filter(|&j| j != i).collect());
500        }
501        return Ok(separators);
502    }
503
504    let k = usize::try_from(conn).map_err(|_| {
505        IgraphError::InvalidArgument("minimum_size_separators: connectivity overflow".into())
506    })?;
507
508    // Work on a simple copy of the graph (multi-edges and loops removed).
509    let mut graph_copy = simplify(graph, true, true)?;
510
511    let mut separators: Vec<Vec<VertexId>> = Vec::new();
512
513    // Step 2: the k largest-degree vertices. If they form a separator,
514    // record it.
515    let x = topk_degree(&graph_copy, k)?;
516    if is_separator(&graph_copy, &x)? {
517        let mut sep = x.clone();
518        sep.sort_unstable();
519        append_unique(&mut separators, vec![sep]);
520    }
521
522    // Build Gbar, the Even–Tarjan reduction; we extend both graph_copy
523    // and Gbar incrementally in step 8. igraph uses an infinite capacity
524    // for the (uncuttable) original edges, but our max-flow only accepts
525    // finite capacities. Replacing ∞ with `n` is exact here: the main
526    // branch only runs for `2 ≤ k ≤ n-2`, so any value-`k` min-cut is
527    // composed solely of unit-capacity vertex-split edges — an original
528    // (or step-8-added) edge of capacity `n > k` can never appear in it.
529    let reduction = even_tarjan_reduction(&graph_copy)?;
530    let mut gbar = reduction.graph;
531    let big = f64::from(no_of_nodes);
532    let mut capacity: Vec<f64> = reduction
533        .capacity
534        .into_iter()
535        .map(|c| if c.is_finite() { c } else { big })
536        .collect();
537
538    // Steps 3–8: for each x_i and each non-adjacent j, find all minimum
539    // (x_i, j) vertex cuts of size k.
540    // `conn` is in `[2, no_of_nodes - 2]` here, so it always fits in a u32.
541    let k_f = f64::from(u32::try_from(conn).map_err(|_| {
542        IgraphError::InvalidArgument("minimum_size_separators: connectivity overflow".into())
543    })?);
544    for &xi in &x {
545        for j in 0..no_of_nodes {
546            if xi == j {
547                continue;
548            }
549            if are_adjacent(&graph_copy, xi, j)? {
550                continue;
551            }
552
553            // Step 4: max flow in Gbar from x_i'' (= xi + n) to j'.
554            let source = xi + no_of_nodes;
555            let target = j;
556            let phivalue = max_flow_value(&gbar, source, target, Some(&capacity))?;
557
558            if (phivalue - k_f).abs() < 0.5 {
559                // Steps 5–7: enumerate all minimum (x_i, j) cuts. Each
560                // cut is a set of unit-capacity vertex-split edges; in
561                // the Even–Tarjan reduction edge id e (< n) is the split
562                // edge of vertex e, so cut edge ids map directly to the
563                // separating vertex set.
564                let cuts = all_st_mincuts(&gbar, source, target, Some(&capacity))?;
565                let mapped: Vec<Vec<VertexId>> = cuts
566                    .cuts
567                    .into_iter()
568                    .map(|cut| {
569                        let mut sep: Vec<VertexId> = cut;
570                        sep.sort_unstable();
571                        sep
572                    })
573                    .collect();
574                append_unique(&mut separators, mapped);
575            }
576
577            // Step 8: add edge (x_i, j) so later flows find new cuts only.
578            graph_copy.add_edge(xi, j)?;
579            gbar.add_edge(xi + no_of_nodes, j)?;
580            gbar.add_edge(j + no_of_nodes, xi)?;
581            capacity.push(big);
582            capacity.push(big);
583        }
584    }
585
586    Ok(separators)
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[test]
594    fn empty_graph() {
595        let g = Graph::with_vertices(0);
596        assert!(!is_separator(&g, &[]).unwrap());
597    }
598
599    #[test]
600    fn singleton_not_separator() {
601        let g = Graph::with_vertices(1);
602        assert!(!is_separator(&g, &[0]).unwrap());
603    }
604
605    #[test]
606    fn path_middle_is_separator() {
607        let mut g = Graph::with_vertices(3);
608        g.add_edge(0, 1).unwrap();
609        g.add_edge(1, 2).unwrap();
610        assert!(is_separator(&g, &[1]).unwrap());
611    }
612
613    #[test]
614    fn path_leaf_not_separator() {
615        let mut g = Graph::with_vertices(3);
616        g.add_edge(0, 1).unwrap();
617        g.add_edge(1, 2).unwrap();
618        assert!(!is_separator(&g, &[0]).unwrap());
619        assert!(!is_separator(&g, &[2]).unwrap());
620    }
621
622    #[test]
623    fn triangle_no_single_separator() {
624        let mut g = Graph::with_vertices(3);
625        g.add_edge(0, 1).unwrap();
626        g.add_edge(1, 2).unwrap();
627        g.add_edge(2, 0).unwrap();
628        // No single vertex disconnects a triangle.
629        assert!(!is_separator(&g, &[0]).unwrap());
630        assert!(!is_separator(&g, &[1]).unwrap());
631        assert!(!is_separator(&g, &[2]).unwrap());
632    }
633
634    #[test]
635    fn triangle_pair_not_separator() {
636        let mut g = Graph::with_vertices(3);
637        g.add_edge(0, 1).unwrap();
638        g.add_edge(1, 2).unwrap();
639        g.add_edge(2, 0).unwrap();
640        // Removing two vertices leaves a single vertex — trivially connected.
641        assert!(!is_separator(&g, &[0, 1]).unwrap());
642    }
643
644    #[test]
645    fn cycle_4_opposite_vertices() {
646        // 0-1-2-3-0. {1,3} separates 0 from 2.
647        let mut g = Graph::with_vertices(4);
648        g.add_edge(0, 1).unwrap();
649        g.add_edge(1, 2).unwrap();
650        g.add_edge(2, 3).unwrap();
651        g.add_edge(3, 0).unwrap();
652        assert!(is_separator(&g, &[1, 3]).unwrap());
653    }
654
655    #[test]
656    fn cycle_4_adjacent_not_separator() {
657        // 0-1-2-3-0. {0,1} does NOT disconnect (2-3 is still connected).
658        let mut g = Graph::with_vertices(4);
659        g.add_edge(0, 1).unwrap();
660        g.add_edge(1, 2).unwrap();
661        g.add_edge(2, 3).unwrap();
662        g.add_edge(3, 0).unwrap();
663        assert!(!is_separator(&g, &[0, 1]).unwrap());
664    }
665
666    #[test]
667    fn already_disconnected_empty_set_is_separator() {
668        // Two components: {0,1}, {2,3}. Empty set "separates".
669        let mut g = Graph::with_vertices(4);
670        g.add_edge(0, 1).unwrap();
671        g.add_edge(2, 3).unwrap();
672        assert!(is_separator(&g, &[]).unwrap());
673    }
674
675    #[test]
676    fn k4_articulation() {
677        // K4 minus one edge: 0-1, 0-2, 0-3, 1-2, 2-3 (missing 1-3).
678        // Vertex 2 is not an articulation point because 0 connects to all others.
679        // Actually: adjacencies: 0→{1,2,3}, 1→{0,2}, 2→{0,1,3}, 3→{0,2}
680        // Remove 0 → remaining {1,2,3}: 1-2, 2-3 → connected. Not separator.
681        // Remove 2 → remaining {0,1,3}: 0-1, 0-3 → connected. Not separator.
682        let mut g = Graph::with_vertices(4);
683        g.add_edge(0, 1).unwrap();
684        g.add_edge(0, 2).unwrap();
685        g.add_edge(0, 3).unwrap();
686        g.add_edge(1, 2).unwrap();
687        g.add_edge(2, 3).unwrap();
688        assert!(!is_separator(&g, &[0]).unwrap());
689        assert!(!is_separator(&g, &[2]).unwrap());
690    }
691
692    #[test]
693    fn bowtie_articulation() {
694        // Two triangles sharing vertex 2: {0,1,2} and {2,3,4}.
695        // Vertex 2 is an articulation point → separator.
696        let mut g = Graph::with_vertices(5);
697        g.add_edge(0, 1).unwrap();
698        g.add_edge(1, 2).unwrap();
699        g.add_edge(2, 0).unwrap();
700        g.add_edge(2, 3).unwrap();
701        g.add_edge(3, 4).unwrap();
702        g.add_edge(4, 2).unwrap();
703        assert!(is_separator(&g, &[2]).unwrap());
704    }
705
706    #[test]
707    fn directed_rejected() {
708        let g = Graph::new(3, true).unwrap();
709        assert!(is_separator(&g, &[0]).is_err());
710        assert!(is_minimal_separator(&g, &[0]).is_err());
711    }
712
713    #[test]
714    fn out_of_range_rejected() {
715        let g = Graph::with_vertices(3);
716        assert!(is_separator(&g, &[5]).is_err());
717    }
718
719    // --- Minimal separator tests ---
720
721    #[test]
722    fn minimal_path_middle() {
723        // Path 0-1-2: {1} is a minimal separator.
724        let mut g = Graph::with_vertices(3);
725        g.add_edge(0, 1).unwrap();
726        g.add_edge(1, 2).unwrap();
727        assert!(is_minimal_separator(&g, &[1]).unwrap());
728    }
729
730    #[test]
731    fn minimal_cycle_4_opposite() {
732        // 0-1-2-3-0: {1,3} is a minimal separator.
733        let mut g = Graph::with_vertices(4);
734        g.add_edge(0, 1).unwrap();
735        g.add_edge(1, 2).unwrap();
736        g.add_edge(2, 3).unwrap();
737        g.add_edge(3, 0).unwrap();
738        assert!(is_minimal_separator(&g, &[1, 3]).unwrap());
739    }
740
741    #[test]
742    fn not_minimal_superset() {
743        // Path 0-1-2-3-4: {1,3} is a separator but NOT minimal ({1} alone suffices).
744        let mut g = Graph::with_vertices(5);
745        g.add_edge(0, 1).unwrap();
746        g.add_edge(1, 2).unwrap();
747        g.add_edge(2, 3).unwrap();
748        g.add_edge(3, 4).unwrap();
749        assert!(is_separator(&g, &[1, 3]).unwrap());
750        assert!(!is_minimal_separator(&g, &[1, 3]).unwrap());
751    }
752
753    #[test]
754    fn not_separator_not_minimal() {
755        // Triangle: {0} is not a separator → not a minimal separator.
756        let mut g = Graph::with_vertices(3);
757        g.add_edge(0, 1).unwrap();
758        g.add_edge(1, 2).unwrap();
759        g.add_edge(2, 0).unwrap();
760        assert!(!is_minimal_separator(&g, &[0]).unwrap());
761    }
762
763    #[test]
764    fn minimal_bowtie_articulation() {
765        // Bowtie: {2} is a minimal separator.
766        let mut g = Graph::with_vertices(5);
767        g.add_edge(0, 1).unwrap();
768        g.add_edge(1, 2).unwrap();
769        g.add_edge(2, 0).unwrap();
770        g.add_edge(2, 3).unwrap();
771        g.add_edge(3, 4).unwrap();
772        g.add_edge(4, 2).unwrap();
773        assert!(is_minimal_separator(&g, &[2]).unwrap());
774    }
775
776    // --- all_minimal_st_separators tests ---
777
778    #[test]
779    fn all_min_sep_empty_graph() {
780        let g = Graph::with_vertices(0);
781        let seps = all_minimal_st_separators(&g).unwrap();
782        assert!(seps.is_empty());
783    }
784
785    #[test]
786    fn all_min_sep_single_vertex() {
787        let g = Graph::with_vertices(1);
788        let seps = all_minimal_st_separators(&g).unwrap();
789        assert!(seps.is_empty());
790    }
791
792    #[test]
793    fn all_min_sep_single_edge() {
794        let mut g = Graph::with_vertices(2);
795        g.add_edge(0, 1).unwrap();
796        let seps = all_minimal_st_separators(&g).unwrap();
797        // Complete graph K2 has no separators
798        assert!(seps.is_empty());
799    }
800
801    #[test]
802    fn all_min_sep_path_3() {
803        // Path 0-1-2: {1} is the only minimal (s,t) separator
804        let mut g = Graph::with_vertices(3);
805        g.add_edge(0, 1).unwrap();
806        g.add_edge(1, 2).unwrap();
807        let seps = all_minimal_st_separators(&g).unwrap();
808        assert_eq!(seps.len(), 1);
809        assert_eq!(seps[0], vec![1]);
810    }
811
812    #[test]
813    fn all_min_sep_path_5() {
814        // Path 0-1-2-3-4: separators {1}, {2}, {3}
815        let mut g = Graph::with_vertices(5);
816        g.add_edge(0, 1).unwrap();
817        g.add_edge(1, 2).unwrap();
818        g.add_edge(2, 3).unwrap();
819        g.add_edge(3, 4).unwrap();
820        let seps = all_minimal_st_separators(&g).unwrap();
821        assert_eq!(seps.len(), 3);
822        assert!(seps.contains(&vec![1]));
823        assert!(seps.contains(&vec![2]));
824        assert!(seps.contains(&vec![3]));
825    }
826
827    #[test]
828    fn all_min_sep_cycle_4() {
829        // C4: 0-1-2-3-0. Minimal separators: {0,2} and {1,3}
830        let mut g = Graph::with_vertices(4);
831        g.add_edge(0, 1).unwrap();
832        g.add_edge(1, 2).unwrap();
833        g.add_edge(2, 3).unwrap();
834        g.add_edge(3, 0).unwrap();
835        let seps = all_minimal_st_separators(&g).unwrap();
836        assert_eq!(seps.len(), 2);
837        assert!(seps.contains(&vec![0, 2]));
838        assert!(seps.contains(&vec![1, 3]));
839    }
840
841    #[test]
842    fn all_min_sep_pentagon_with_chord() {
843        // 0-1, 1-2, 2-3, 3-4, 4-1 (pentagon where vertex 1 connects to 0 and 4)
844        let mut g = Graph::with_vertices(5);
845        g.add_edge(0, 1).unwrap();
846        g.add_edge(1, 2).unwrap();
847        g.add_edge(2, 3).unwrap();
848        g.add_edge(3, 4).unwrap();
849        g.add_edge(4, 1).unwrap();
850        let seps = all_minimal_st_separators(&g).unwrap();
851        // Should contain {1}, {2,4}, {1,3}
852        assert_eq!(seps.len(), 3);
853        assert!(seps.contains(&vec![1]));
854        assert!(seps.contains(&vec![2, 4]));
855        assert!(seps.contains(&vec![1, 3]));
856    }
857
858    #[test]
859    fn all_min_sep_bowtie() {
860        // Bowtie: triangles {0,1,2} and {2,3,4} sharing vertex 2
861        let mut g = Graph::with_vertices(5);
862        g.add_edge(0, 1).unwrap();
863        g.add_edge(1, 2).unwrap();
864        g.add_edge(2, 0).unwrap();
865        g.add_edge(2, 3).unwrap();
866        g.add_edge(3, 4).unwrap();
867        g.add_edge(4, 2).unwrap();
868        let seps = all_minimal_st_separators(&g).unwrap();
869        // Only separator is {2}
870        assert_eq!(seps.len(), 1);
871        assert_eq!(seps[0], vec![2]);
872    }
873
874    #[test]
875    fn all_min_sep_complete_graph() {
876        // K4: no vertex set can be a minimal separator
877        let mut g = Graph::with_vertices(4);
878        g.add_edge(0, 1).unwrap();
879        g.add_edge(0, 2).unwrap();
880        g.add_edge(0, 3).unwrap();
881        g.add_edge(1, 2).unwrap();
882        g.add_edge(1, 3).unwrap();
883        g.add_edge(2, 3).unwrap();
884        let seps = all_minimal_st_separators(&g).unwrap();
885        assert!(seps.is_empty());
886    }
887
888    #[test]
889    fn all_min_sep_disconnected() {
890        // Two disconnected edges: 0-1, 2-3
891        let mut g = Graph::with_vertices(4);
892        g.add_edge(0, 1).unwrap();
893        g.add_edge(2, 3).unwrap();
894        let seps = all_minimal_st_separators(&g).unwrap();
895        // Empty set separates. Also {0}, {1}, {2}, {3} separate.
896        // But the algorithm finds minimal (s,t) separators which can include
897        // empty set — however igraph convention skips empty separators.
898        // Just check we don't crash and all returned are non-empty.
899        for s in &seps {
900            assert!(!s.is_empty());
901        }
902    }
903
904    // --- Minimum-size separator tests (Kanevsky) ---
905
906    /// Canonicalise: sort each separator ascending, then sort the list.
907    fn canon(mut seps: Vec<Vec<VertexId>>) -> Vec<Vec<VertexId>> {
908        for s in &mut seps {
909            s.sort_unstable();
910        }
911        seps.sort();
912        seps
913    }
914
915    fn undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
916        let mut g = Graph::with_vertices(n);
917        for &(a, b) in edges {
918            g.add_edge(a, b).unwrap();
919        }
920        g
921    }
922
923    #[test]
924    fn mss_directed_rejected() {
925        let g = Graph::new(3, true).unwrap();
926        assert!(minimum_size_separators(&g).is_err());
927    }
928
929    #[test]
930    fn mss_path3() {
931        // 0-1-2: vertex 1 is the unique minimum separator.
932        let g = undirected(3, &[(0, 1), (1, 2)]);
933        assert_eq!(canon(minimum_size_separators(&g).unwrap()), vec![vec![1]]);
934    }
935
936    #[test]
937    fn mss_disconnected_empty() {
938        // Already disconnected ⇒ no separators (igraph convention).
939        let g = undirected(4, &[(0, 1), (2, 3)]);
940        assert!(minimum_size_separators(&g).unwrap().is_empty());
941    }
942
943    #[test]
944    fn mss_articulation_singletons() {
945        // Bowtie: two triangles sharing vertex 2. conn == 1, the only
946        // articulation point is vertex 2.
947        let g = undirected(5, &[(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (2, 4)]);
948        assert_eq!(canon(minimum_size_separators(&g).unwrap()), vec![vec![2]]);
949    }
950
951    #[test]
952    fn mss_complete_k4() {
953        // K4 (conn == n-1 == 3): every 3-subset of vertices is a minimum
954        // separator.
955        let g = undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
956        assert_eq!(
957            canon(minimum_size_separators(&g).unwrap()),
958            vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 2, 3], vec![1, 2, 3],]
959        );
960    }
961
962    #[test]
963    fn mss_complete_k3() {
964        // K3 (conn == n-1 == 2): every pair is a minimum separator.
965        let g = undirected(3, &[(0, 1), (0, 2), (1, 2)]);
966        assert_eq!(
967            canon(minimum_size_separators(&g).unwrap()),
968            vec![vec![0, 1], vec![0, 2], vec![1, 2]]
969        );
970    }
971
972    #[test]
973    fn mss_cycle5() {
974        // C5: each pair of non-adjacent vertices is a minimum 2-separator.
975        let g = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
976        assert_eq!(
977            canon(minimum_size_separators(&g).unwrap()),
978            vec![vec![0, 2], vec![0, 3], vec![1, 3], vec![1, 4], vec![2, 4],]
979        );
980    }
981
982    #[test]
983    fn mss_grid3x3() {
984        // 3×3 grid; the four 2-separators are the mid-edge pairs.
985        let g = undirected(
986            9,
987            &[
988                (0, 1),
989                (1, 2),
990                (3, 4),
991                (4, 5),
992                (6, 7),
993                (7, 8),
994                (0, 3),
995                (3, 6),
996                (1, 4),
997                (4, 7),
998                (2, 5),
999                (5, 8),
1000            ],
1001        );
1002        assert_eq!(
1003            canon(minimum_size_separators(&g).unwrap()),
1004            vec![vec![1, 3], vec![1, 5], vec![3, 7], vec![5, 7]]
1005        );
1006    }
1007
1008    #[test]
1009    fn mss_complete_bipartite_k23() {
1010        // K_{2,3}: the two-vertex side {0,1} is the unique minimum
1011        // separator.
1012        let g = undirected(5, &[(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)]);
1013        assert_eq!(
1014            canon(minimum_size_separators(&g).unwrap()),
1015            vec![vec![0, 1]]
1016        );
1017    }
1018
1019    #[test]
1020    fn mss_separators_are_valid() {
1021        // Every returned set must actually separate the graph, and all
1022        // must share the same (minimum) cardinality.
1023        let g = undirected(
1024            7,
1025            &[
1026                (0, 1),
1027                (1, 2),
1028                (2, 0),
1029                (2, 3),
1030                (3, 4),
1031                (4, 5),
1032                (5, 3),
1033                (5, 6),
1034                (6, 3),
1035            ],
1036        );
1037        let seps = minimum_size_separators(&g).unwrap();
1038        assert!(!seps.is_empty());
1039        let k = seps[0].len();
1040        for s in &seps {
1041            assert_eq!(s.len(), k, "all minimum separators share size");
1042            assert!(is_separator(&g, s).unwrap(), "{s:?} must separate");
1043        }
1044    }
1045}