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