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