Skip to main content

rust_igraph/algorithms/flow/
all_st_mincuts.rs

1//! `all_st_mincuts` (ALGO-FL-032) — list every *minimum* (s,t) edge cut of a
2//! directed graph.
3//!
4//! Counterpart of `igraph_all_st_mincuts` from
5//! `references/igraph/src/flow/st-cuts.c:1335`.
6//!
7//! Like [`all_st_cuts`](super::all_st_cuts) this uses the Provan-Shier
8//! paradigm (J. S. Provan and D. R. Shier, *A paradigm for listing
9//! (s,t)-cuts in graphs*, Algorithmica 15:351–372, 1996), but it enumerates
10//! only the cuts of *minimum total capacity*. The pipeline:
11//!
12//! 1. Compute a maximum `source → target` flow (value + per-edge flow).
13//! 2. Build the **reverse residual** graph of that flow.
14//! 3. Contract the residual by its **strongly connected components** and
15//!    simplify, then relabel into topological order. Every minimum cut
16//!    corresponds to a *closed set* (down-set) of this contracted DAG.
17//! 4. Mark the projected vertices that are endpoints of a positive-flow edge
18//!    as *active*.
19//! 5. Run the shared Provan-Shier recursion
20//!    ([`provan_shier_list`]) with an active-set pivot that walks in-neighbours
21//!    of the topologically ordered residual.
22//! 6. Map each closed set back to original vertices; the cut edges are the
23//!    positive-flow edges leaving the source-side set.
24
25// The contracted/residual relabelling maps go through `u32`/`usize` casts
26// bounded by `vcount`, which `max_flow` already constrains.
27#![allow(clippy::cast_possible_truncation)]
28
29use std::collections::VecDeque;
30
31use crate::algorithms::connectivity::strong::strongly_connected_components;
32use crate::algorithms::flow::max_flow::max_flow;
33use crate::algorithms::flow::provan_shier::{EStack, MarkedQueue, Pivot, provan_shier_list};
34use crate::algorithms::operators::contract_vertices::contract_vertices;
35use crate::algorithms::operators::permute_vertices::permute_vertices;
36use crate::algorithms::operators::residual_graph::reverse_residual_graph;
37use crate::algorithms::operators::simplify::simplify;
38use crate::algorithms::paths::dijkstra::DijkstraMode;
39use crate::algorithms::properties::topological_sorting::topological_sorting;
40use crate::core::graph::EdgeId;
41use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
42
43/// Result of [`all_st_mincuts`].
44///
45/// `cuts[i]` and `partition1s[i]` describe the same minimum cut:
46/// `partition1s[i]` is the source-side vertex set `X` (always contains
47/// `source`, never `target`), and `cuts[i]` lists the ids of the
48/// positive-flow edges leaving `X` (`from ∈ X`, `to ∉ X`). Every listed cut
49/// has total capacity equal to [`value`](Self::value). Vertices within each
50/// partition and edge ids within each cut are sorted ascending.
51#[derive(Clone, Debug, PartialEq)]
52pub struct StMinCuts {
53    /// The minimum cut value (maximum `source → target` flow).
54    pub value: f64,
55    /// Edge-id list of each minimum cut, sorted ascending.
56    pub cuts: Vec<Vec<EdgeId>>,
57    /// Source-side vertex set generating each cut, sorted ascending.
58    pub partition1s: Vec<Vec<VertexId>>,
59}
60
61/// Pivot for listing **minimum** (s,t) cuts (`igraph_i_all_st_mincuts_pivot`).
62///
63/// Operates on the topologically ordered, SCC-contracted reverse residual
64/// graph. A vertex is *active* iff it is an endpoint of a positive-flow edge
65/// in the original graph (projected through the contraction).
66struct MincutsPivot {
67    /// In-neighbour adjacency of the residual graph (new/topological ids).
68    in_adj: Vec<Vec<VertexId>>,
69    /// `active[v]` — `v` is an endpoint of a positive-flow projected edge.
70    active: Vec<bool>,
71}
72
73impl MincutsPivot {
74    fn new(residual: &Graph, active: Vec<bool>) -> IgraphResult<Self> {
75        let n = residual.vcount() as usize;
76        let mut in_adj = vec![Vec::new(); n];
77        for (v, slot) in in_adj.iter_mut().enumerate() {
78            *slot = residual.in_neighbors_vec(v as VertexId)?;
79        }
80        Ok(Self { in_adj, active })
81    }
82
83    /// `igraph_i_all_st_mincuts_minimal`: the set of active vertices that are
84    /// minimal w.r.t. reachability in the residual DAG (no in-path from
85    /// another already-minimal vertex). The residual is in topological order,
86    /// so a single forward pass suffices.
87    fn minimal(&self, s: &MarkedQueue) -> Vec<VertexId> {
88        let n = self.in_adj.len();
89        let mut connected = vec![false; n];
90        let mut minimal: Vec<VertexId> = Vec::new();
91        for i in 0..n {
92            if s.iselement(i as VertexId) {
93                continue;
94            }
95            let conn = self.in_adj[i].iter().any(|&nei| connected[nei as usize]);
96            if conn {
97                connected[i] = true;
98            } else if self.active[i] {
99                minimal.push(i as VertexId);
100                connected[i] = true;
101            }
102        }
103        minimal
104    }
105
106    /// In-direction BFS from `root`, visiting only vertices in `keep`.
107    fn bfs_in(&self, root: VertexId, keep: &[bool]) -> Vec<VertexId> {
108        let n = self.in_adj.len();
109        let mut visited = vec![false; n];
110        let mut order: Vec<VertexId> = Vec::new();
111        if !keep[root as usize] {
112            return order;
113        }
114        let mut queue: VecDeque<VertexId> = VecDeque::new();
115        visited[root as usize] = true;
116        order.push(root);
117        queue.push_back(root);
118        while let Some(u) = queue.pop_front() {
119            for &nei in &self.in_adj[u as usize] {
120                if keep[nei as usize] && !visited[nei as usize] {
121                    visited[nei as usize] = true;
122                    order.push(nei);
123                    queue.push_back(nei);
124                }
125            }
126        }
127        order
128    }
129}
130
131impl Pivot for MincutsPivot {
132    fn pivot(
133        &mut self,
134        s: &MarkedQueue,
135        t: &EStack,
136        _source: VertexId,
137        target: VertexId,
138    ) -> IgraphResult<(VertexId, Vec<VertexId>)> {
139        let n = self.in_adj.len();
140        if s.size() == n {
141            return Ok((0, Vec::new()));
142        }
143
144        let mut keep = vec![false; n];
145        for (i, slot) in keep.iter_mut().enumerate() {
146            *slot = !s.iselement(i as VertexId);
147        }
148
149        let m = self.minimal(s);
150
151        // Find a minimal element that is neither the target nor forbidden.
152        let chosen = m
153            .iter()
154            .copied()
155            .find(|&min| min != target && !t.iselement(min));
156
157        if let Some(v) = chosen {
158            // I(S,v): all residual vertices that can reach v (within `keep`)
159            // and are not already in S.
160            let isv_min = self.bfs_in(v, &keep);
161            let isv: Vec<VertexId> = isv_min.into_iter().filter(|&u| !s.iselement(u)).collect();
162            return Ok((v, isv));
163        }
164
165        Ok((0, Vec::new()))
166    }
167}
168
169/// List all *minimum* (s,t) edge cuts of a directed graph (Provan-Shier).
170///
171/// # Arguments
172///
173/// * `graph`    — directed input graph. Undirected graphs are rejected.
174/// * `source`   — source vertex id.
175/// * `target`   — target vertex id (must differ from `source`).
176/// * `capacity` — optional per-edge capacities in edge-id order. When `None`,
177///   every edge has unit capacity. All capacities must be strictly positive.
178///
179/// # Errors
180///
181/// * [`IgraphError::InvalidArgument`] when `graph` is undirected, when
182///   `source == target`, when `capacity.len() != ecount()`, or when any
183///   capacity is not strictly positive.
184/// * [`IgraphError::VertexOutOfRange`] when `source` or `target` is not a
185///   valid vertex id.
186///
187/// [`IgraphError::InvalidArgument`]: crate::core::IgraphError::InvalidArgument
188/// [`IgraphError::VertexOutOfRange`]: crate::core::IgraphError::VertexOutOfRange
189///
190/// # Examples
191///
192/// ```
193/// use rust_igraph::all_st_mincuts;
194/// use rust_igraph::Graph;
195///
196/// // Path 0→1→2→3→4: the single min cut value is 1 and there are four
197/// // distinct minimum cuts, one per edge.
198/// let mut g = Graph::new(5, true).unwrap();
199/// for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
200///     g.add_edge(u, v).unwrap();
201/// }
202/// let res = all_st_mincuts(&g, 0, 4, None).unwrap();
203/// assert_eq!(res.value, 1.0);
204/// assert_eq!(res.cuts.len(), 4);
205/// ```
206pub fn all_st_mincuts(
207    graph: &Graph,
208    source: VertexId,
209    target: VertexId,
210    capacity: Option<&[f64]>,
211) -> IgraphResult<StMinCuts> {
212    let n = graph.vcount();
213    let m = graph.ecount();
214
215    if !graph.is_directed() {
216        return Err(IgraphError::InvalidArgument(
217            "all_st_mincuts: listing s-t cuts is only implemented for directed graphs".to_string(),
218        ));
219    }
220    if source >= n {
221        return Err(IgraphError::VertexOutOfRange { id: source, n });
222    }
223    if target >= n {
224        return Err(IgraphError::VertexOutOfRange { id: target, n });
225    }
226    if source == target {
227        return Err(IgraphError::InvalidArgument(
228            "all_st_mincuts: source and target must differ".to_string(),
229        ));
230    }
231    if let Some(cap) = capacity {
232        if cap.len() != m {
233            return Err(IgraphError::InvalidArgument(format!(
234                "all_st_mincuts: capacity length {} != edge count {m}",
235                cap.len()
236            )));
237        }
238        if cap.iter().any(|&c| !c.is_finite() || c <= 0.0) {
239            return Err(IgraphError::InvalidArgument(
240                "all_st_mincuts: all capacities must be strictly positive".to_string(),
241            ));
242        }
243    }
244
245    // 1. Maximum flow.
246    let mf = max_flow(graph, source, target, capacity)?;
247    let value = mf.value;
248    let flow = mf.flow;
249
250    // 2. Reverse residual graph.
251    let residual = reverse_residual_graph(graph, capacity, &flow)?;
252
253    // 3. Contract strongly connected components, simplify, relabel into
254    //    topological order.
255    let scc = strongly_connected_components(&residual)?;
256    let mut ntol = scc.membership; // original vertex -> contracted (SCC) id
257    let residual = contract_vertices(&residual, &ntol)?;
258    let residual = simplify(&residual, true, true)?;
259
260    let n_res = residual.vcount() as usize;
261    let order = topological_sorting(&residual, DijkstraMode::Out)?; // order[pos] = old id
262    let mut inv_order = vec![0u32; n_res];
263    for (pos, &v) in order.iter().enumerate() {
264        inv_order[v as usize] = pos as u32;
265    }
266    // Relabel the projection map so contracted ids follow topological order.
267    for slot in &mut ntol {
268        *slot = inv_order[*slot as usize];
269    }
270    // `permute_vertices(perm)[new] = old`, so feeding `order` makes new id
271    // `pos` correspond to old id `order[pos]` — exactly the topological
272    // numbering captured in `inv_order`.
273    let residual = permute_vertices(&residual, &order)?;
274
275    let newsource = ntol[source as usize];
276    let newtarget = ntol[target as usize];
277
278    // 4. Active projected vertices: endpoints of a positive-flow edge.
279    let mut active = vec![false; n_res];
280    for (e, &f) in flow.iter().enumerate() {
281        if f > 0.0 {
282            let (from, to) = graph.edge(e as EdgeId)?;
283            active[ntol[from as usize] as usize] = true;
284            active[ntol[to as usize] as usize] = true;
285        }
286    }
287
288    // 5. Enumerate closed sets via the shared Provan-Shier recursion.
289    let mut pivot = MincutsPivot::new(&residual, active)?;
290    let closedsets = provan_shier_list(n_res, newsource, newtarget, &mut pivot)?;
291
292    // 6a. Expand each closed set (contracted ids) back to original vertices.
293    let mut revmap: Vec<Vec<VertexId>> = vec![Vec::new(); n_res];
294    for i in 0..n as usize {
295        revmap[ntol[i] as usize].push(i as VertexId);
296    }
297
298    let mut partition1s: Vec<Vec<VertexId>> = Vec::with_capacity(closedsets.len());
299    for supercut in &closedsets {
300        let mut part: Vec<VertexId> = Vec::new();
301        for &vtx in supercut {
302            part.extend_from_slice(&revmap[vtx as usize]);
303        }
304        part.sort_unstable();
305        partition1s.push(part);
306    }
307
308    // 6b. The cut edges are the positive-flow edges leaving each partition.
309    let mut cuts: Vec<Vec<EdgeId>> = Vec::with_capacity(partition1s.len());
310    let mut in_s = vec![false; n as usize];
311    for part in &partition1s {
312        for &v in part {
313            in_s[v as usize] = true;
314        }
315        let mut cut: Vec<EdgeId> = Vec::new();
316        for (e, &f) in flow.iter().enumerate() {
317            if f > 0.0 {
318                let (from, to) = graph.edge(e as EdgeId)?;
319                if in_s[from as usize] && !in_s[to as usize] {
320                    cut.push(e as EdgeId);
321                }
322            }
323        }
324        cut.sort_unstable();
325        cuts.push(cut);
326        for &v in part {
327            in_s[v as usize] = false;
328        }
329    }
330
331    Ok(StMinCuts {
332        value,
333        cuts,
334        partition1s,
335    })
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use std::collections::HashSet;
342
343    fn assert_close(actual: f64, expected: f64) {
344        assert!(
345            (actual - expected).abs() < 1e-12,
346            "actual = {actual}, expected = {expected}"
347        );
348    }
349
350    fn build(n: u32, edges: &[(u32, u32)]) -> Graph {
351        let mut g = Graph::new(n, true).expect("directed graph");
352        for &(u, v) in edges {
353            g.add_edge(u, v).expect("add edge");
354        }
355        g
356    }
357
358    /// A canonicalised minimum cut: `(sorted partition, sorted endpoint pairs)`.
359    type CanonCut = (Vec<u32>, Vec<(u32, u32)>);
360    /// `(partition slice, cut-endpoint slice)` literals for the expected set.
361    type ExpectedPairs<'a> = [(&'a [u32], &'a [(u32, u32)])];
362
363    /// Canonical set of `(sorted partition, sorted cut-endpoint-pairs)`.
364    /// The set of minimum cuts is unique, so this comparison is independent
365    /// of enumeration order and internal vertex relabelling.
366    fn cut_set(g: &Graph, res: &StMinCuts) -> HashSet<CanonCut> {
367        let mut out = HashSet::new();
368        for (part, cut) in res.partition1s.iter().zip(res.cuts.iter()) {
369            let mut endpoints: Vec<(u32, u32)> =
370                cut.iter().map(|&e| g.edge(e).expect("edge")).collect();
371            endpoints.sort_unstable();
372            out.insert((part.clone(), endpoints));
373        }
374        out
375    }
376
377    fn expected_set(pairs: &ExpectedPairs) -> HashSet<CanonCut> {
378        pairs
379            .iter()
380            .map(|(part, cut)| {
381                let mut p = part.to_vec();
382                p.sort_unstable();
383                let mut c = cut.to_vec();
384                c.sort_unstable();
385                (p, c)
386            })
387            .collect()
388    }
389
390    /// Every cut must (a) have total positive-flow weight == value and
391    /// (b) actually disconnect target from source.
392    fn assert_consistent(g: &Graph, source: VertexId, target: VertexId, res: &StMinCuts) {
393        assert_eq!(res.cuts.len(), res.partition1s.len());
394        let n = g.vcount() as usize;
395        for (part, cut) in res.partition1s.iter().zip(res.cuts.iter()) {
396            assert!(part.contains(&source), "source missing from {part:?}");
397            assert!(!part.contains(&target), "target present in {part:?}");
398            // Removing the cut disconnects target from source.
399            let removed: HashSet<EdgeId> = cut.iter().copied().collect();
400            let mut adj = vec![Vec::new(); n];
401            for e in 0..g.ecount() as EdgeId {
402                if removed.contains(&e) {
403                    continue;
404                }
405                let (from, to) = g.edge(e).expect("edge");
406                adj[from as usize].push(to);
407            }
408            let mut visited = vec![false; n];
409            let mut stack = vec![source];
410            visited[source as usize] = true;
411            while let Some(u) = stack.pop() {
412                for &w in &adj[u as usize] {
413                    if !visited[w as usize] {
414                        visited[w as usize] = true;
415                        stack.push(w);
416                    }
417                }
418            }
419            assert!(
420                !visited[target as usize],
421                "target reachable after removing cut {cut:?}"
422            );
423        }
424    }
425
426    #[test]
427    fn graph1_path_four_cuts() {
428        let g = build(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
429        let res = all_st_mincuts(&g, 0, 4, None).expect("compute");
430        assert_close(res.value, 1.0);
431        let expected = expected_set(&[
432            (&[0], &[(0, 1)]),
433            (&[0, 1], &[(1, 2)]),
434            (&[0, 1, 2], &[(2, 3)]),
435            (&[0, 1, 2, 3], &[(3, 4)]),
436        ]);
437        assert_eq!(cut_set(&g, &res), expected);
438        assert_consistent(&g, 0, 4, &res);
439    }
440
441    #[test]
442    fn graph2_diamond_two_cuts() {
443        let g = build(6, &[(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]);
444        let res = all_st_mincuts(&g, 0, 5, None).expect("compute");
445        assert_close(res.value, 1.0);
446        let expected = expected_set(&[(&[0], &[(0, 1)]), (&[0, 1, 2, 3, 4], &[(4, 5)])]);
447        assert_eq!(cut_set(&g, &res), expected);
448        assert_consistent(&g, 0, 5, &res);
449    }
450
451    #[test]
452    fn graph3_single_cut() {
453        let g = build(6, &[(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]);
454        let res = all_st_mincuts(&g, 0, 4, None).expect("compute");
455        assert_close(res.value, 1.0);
456        let expected = expected_set(&[(&[0], &[(0, 1)])]);
457        assert_eq!(cut_set(&g, &res), expected);
458        assert_consistent(&g, 0, 4, &res);
459    }
460
461    #[test]
462    fn graph4_parallel_middle_three_cuts() {
463        let g = build(
464            9,
465            &[
466                (0, 1),
467                (0, 2),
468                (1, 3),
469                (2, 3),
470                (1, 4),
471                (4, 2),
472                (1, 5),
473                (5, 2),
474                (1, 6),
475                (6, 2),
476                (1, 7),
477                (7, 2),
478                (1, 8),
479                (8, 2),
480            ],
481        );
482        let res = all_st_mincuts(&g, 0, 3, None).expect("compute");
483        assert_close(res.value, 2.0);
484        let expected = expected_set(&[
485            (&[0], &[(0, 1), (0, 2)]),
486            (&[0, 2], &[(0, 1), (2, 3)]),
487            (&[0, 1, 2, 4, 5, 6, 7, 8], &[(1, 3), (2, 3)]),
488        ]);
489        assert_eq!(cut_set(&g, &res), expected);
490        assert_consistent(&g, 0, 3, &res);
491    }
492
493    #[test]
494    fn graph5_reverse_two_cuts() {
495        let g = build(4, &[(1, 0), (2, 0), (2, 1), (3, 2)]);
496        let res = all_st_mincuts(&g, 2, 0, None).expect("compute");
497        assert_close(res.value, 2.0);
498        let expected = expected_set(&[(&[2], &[(2, 0), (2, 1)]), (&[2, 1], &[(1, 0), (2, 0)])]);
499        assert_eq!(cut_set(&g, &res), expected);
500        assert_consistent(&g, 2, 0, &res);
501    }
502
503    #[test]
504    fn graph7_chain_five_cuts() {
505        let g = build(
506            9,
507            &[
508                (0, 4),
509                (0, 7),
510                (1, 6),
511                (2, 1),
512                (3, 8),
513                (4, 0),
514                (4, 2),
515                (4, 5),
516                (5, 0),
517                (5, 3),
518                (6, 7),
519                (7, 8),
520            ],
521        );
522        let res = all_st_mincuts(&g, 0, 8, None).expect("compute");
523        assert_close(res.value, 2.0);
524        let expected = expected_set(&[
525            (&[0], &[(0, 4), (0, 7)]),
526            (&[0, 7], &[(0, 4), (7, 8)]),
527            (&[0, 7, 4, 2, 1, 6], &[(4, 5), (7, 8)]),
528            (&[0, 7, 4, 2, 1, 6, 5], &[(5, 3), (7, 8)]),
529            (&[0, 7, 4, 2, 1, 6, 5, 3], &[(3, 8), (7, 8)]),
530        ]);
531        assert_eq!(cut_set(&g, &res), expected);
532        assert_consistent(&g, 0, 8, &res);
533    }
534
535    #[test]
536    fn rejects_undirected_graph() {
537        let g = Graph::with_vertices(4);
538        let err = all_st_mincuts(&g, 0, 3, None).expect_err("must reject undirected");
539        assert!(matches!(err, IgraphError::InvalidArgument(_)));
540    }
541
542    #[test]
543    fn rejects_equal_source_and_target() {
544        let g = build(3, &[(0, 1), (1, 2)]);
545        let err = all_st_mincuts(&g, 1, 1, None).expect_err("must reject s == t");
546        assert!(matches!(err, IgraphError::InvalidArgument(_)));
547    }
548
549    #[test]
550    fn rejects_out_of_range_endpoints() {
551        let g = build(3, &[(0, 1), (1, 2)]);
552        match all_st_mincuts(&g, 5, 1, None).expect_err("bad source") {
553            IgraphError::VertexOutOfRange { id, n } => assert_eq!((id, n), (5, 3)),
554            other => panic!("unexpected error: {other:?}"),
555        }
556        match all_st_mincuts(&g, 0, 9, None).expect_err("bad target") {
557            IgraphError::VertexOutOfRange { id, n } => assert_eq!((id, n), (9, 3)),
558            other => panic!("unexpected error: {other:?}"),
559        }
560    }
561
562    #[test]
563    fn rejects_bad_capacity() {
564        let g = build(3, &[(0, 1), (1, 2)]);
565        // Wrong length.
566        let err = all_st_mincuts(&g, 0, 2, Some(&[1.0])).expect_err("bad length");
567        assert!(matches!(err, IgraphError::InvalidArgument(_)));
568        // Non-positive capacity.
569        let err = all_st_mincuts(&g, 0, 2, Some(&[1.0, 0.0])).expect_err("non-positive");
570        assert!(matches!(err, IgraphError::InvalidArgument(_)));
571    }
572}