Skip to main content

weavatrix_graph/algo/cuts/stoer_wagner/
mod.rs

1mod candidate;
2mod queue;
3
4use self::candidate::{Candidate, better, canonical_candidate, map_nodes, nodes, validate_weight};
5use self::queue::MaxQueue;
6use crate::{GraphError, IndexUndirectedGraphView, Measure, Result, Vec};
7use alloc::collections::BTreeMap;
8use core::cmp::Ordering;
9
10/// A deterministic global minimum cut and both sides of its partition.
11#[derive(Debug, Clone, PartialEq)]
12pub struct StoerWagnerCut<Node, Weight> {
13    weight: Weight,
14    partition: Vec<Node>,
15    complement: Vec<Node>,
16}
17
18impl<Node, Weight: Copy> StoerWagnerCut<Node, Weight> {
19    #[must_use]
20    pub const fn weight(&self) -> Weight {
21        self.weight
22    }
23
24    #[must_use]
25    pub fn partition(&self) -> &[Node] {
26        &self.partition
27    }
28
29    #[must_use]
30    pub fn complement(&self) -> &[Node] {
31        &self.complement
32    }
33
34    #[must_use]
35    pub fn into_parts(self) -> (Weight, Vec<Node>, Vec<Node>) {
36        (self.weight, self.partition, self.complement)
37    }
38}
39
40struct WorkingCut<M> {
41    adjacency: Vec<BTreeMap<usize, M>>,
42    active: Vec<bool>,
43    groups: Vec<Vec<usize>>,
44    active_count: usize,
45}
46
47/// Finds the global minimum cut of an undirected weighted multigraph.
48///
49/// Parallel edge weights are added with overflow checking; self-loops do not
50/// affect the cut. Empty and singleton graphs return `Ok(None)`.
51///
52/// # Errors
53///
54/// Returns an error for a negative/non-finite weight or checked-add overflow.
55pub fn stoer_wagner_min_cut<G, F, M>(
56    graph: &G,
57    edge_weight: F,
58) -> Result<Option<StoerWagnerCut<G::Node, M>>>
59where
60    G: IndexUndirectedGraphView,
61    F: FnMut(G::Edge) -> M,
62    M: Measure,
63{
64    stoer_wagner_min_cut_filtered(graph, |_| true, edge_weight)
65}
66
67/// Filtered Stoer-Wagner min-cut. Each predicate and accepted weight callback
68/// is evaluated exactly once per edge.
69///
70/// # Errors
71///
72/// Returns an error for invalid weights or arithmetic overflow.
73pub fn stoer_wagner_min_cut_filtered<G, P, F, M>(
74    graph: &G,
75    mut allows_edge: P,
76    mut edge_weight: F,
77) -> Result<Option<StoerWagnerCut<G::Node, M>>>
78where
79    G: IndexUndirectedGraphView,
80    P: FnMut(G::Edge) -> bool,
81    F: FnMut(G::Edge) -> M,
82    M: Measure,
83{
84    if graph.node_count() < 2 {
85        return Ok(None);
86    }
87    let (nodes_by_slot, nodes) = nodes(graph);
88    let mut working = WorkingCut::new(graph.node_bound(), &nodes);
89    for edge in graph.edge_indices() {
90        if !allows_edge(edge) {
91            continue;
92        }
93        let weight = edge_weight(edge);
94        validate_weight(weight)?;
95        let Some(endpoints) = graph.edge_endpoints(edge) else {
96            continue;
97        };
98        let source = G::node_slot(endpoints.source());
99        let target = G::node_slot(endpoints.target());
100        if source != target {
101            working.add_edge(source, target, weight)?;
102        }
103    }
104    let candidate = working.solve(&nodes)?;
105    Ok(candidate.map(|cut| StoerWagnerCut {
106        weight: cut.weight,
107        partition: map_nodes(&cut.partition, &nodes_by_slot),
108        complement: map_nodes(&cut.complement, &nodes_by_slot),
109    }))
110}
111
112impl<M: Measure> WorkingCut<M> {
113    fn new(node_bound: usize, nodes: &[usize]) -> Self {
114        let mut active = vec![false; node_bound];
115        for &node in nodes {
116            active[node] = true;
117        }
118        let groups = (0..node_bound).map(|node| vec![node]).collect();
119        Self {
120            adjacency: vec![BTreeMap::new(); node_bound],
121            active,
122            groups,
123            active_count: nodes.len(),
124        }
125    }
126
127    fn add_edge(&mut self, source: usize, target: usize, weight: M) -> Result<()> {
128        let sum = self.adjacency[source]
129            .get(&target)
130            .copied()
131            .unwrap_or_else(M::zero)
132            .checked_add(weight)
133            .ok_or(GraphError::ArithmeticOverflow {
134                operation: "Stoer-Wagner edge aggregation",
135            })?;
136        self.adjacency[source].insert(target, sum);
137        self.adjacency[target].insert(source, sum);
138        Ok(())
139    }
140
141    fn solve(&mut self, nodes: &[usize]) -> Result<Option<Candidate<M>>> {
142        let mut best = None;
143        let mut queue = MaxQueue::with_capacity(nodes.len() * 2);
144        let mut weights = vec![M::zero(); self.active.len()];
145        let mut added = vec![false; self.active.len()];
146        while self.active_count > 1 {
147            let (source, target, weight) =
148                self.phase(nodes, &mut queue, &mut weights, &mut added)?;
149            let candidate = canonical_candidate(weight, &self.groups[target], nodes);
150            if better(&candidate, best.as_ref()) {
151                best = Some(candidate);
152            }
153            self.merge(source, target)?;
154        }
155        Ok(best)
156    }
157
158    fn phase(
159        &self,
160        nodes: &[usize],
161        queue: &mut MaxQueue<M>,
162        weights: &mut [M],
163        added: &mut [bool],
164    ) -> Result<(usize, usize, M)> {
165        weights.fill(M::zero());
166        added.fill(false);
167        queue.clear();
168        for &node in nodes {
169            if self.active[node] {
170                queue.push(node, M::zero());
171            }
172        }
173        let mut previous = None;
174        for index in 0..self.active_count {
175            let (node, weight) = pop_current(queue, &self.active, added, weights);
176            if index + 1 == self.active_count {
177                return Ok((previous.expect("phase has two nodes"), node, weight));
178            }
179            added[node] = true;
180            previous = Some(node);
181            for (&neighbor, &edge_weight) in &self.adjacency[node] {
182                if self.active[neighbor] && !added[neighbor] {
183                    weights[neighbor] = weights[neighbor].checked_add(edge_weight).ok_or(
184                        GraphError::ArithmeticOverflow {
185                            operation: "Stoer-Wagner phase",
186                        },
187                    )?;
188                    queue.push(neighbor, weights[neighbor]);
189                }
190            }
191        }
192        unreachable!("active phase always selects a target")
193    }
194
195    fn merge(&mut self, source: usize, target: usize) -> Result<()> {
196        let removed = core::mem::take(&mut self.adjacency[target]);
197        self.adjacency[source].remove(&target);
198        for (neighbor, weight) in removed {
199            self.adjacency[neighbor].remove(&target);
200            if neighbor == source || !self.active[neighbor] {
201                continue;
202            }
203            let sum = self.adjacency[source]
204                .get(&neighbor)
205                .copied()
206                .unwrap_or_else(M::zero)
207                .checked_add(weight)
208                .ok_or(GraphError::ArithmeticOverflow {
209                    operation: "Stoer-Wagner contraction",
210                })?;
211            self.adjacency[source].insert(neighbor, sum);
212            self.adjacency[neighbor].insert(source, sum);
213        }
214        self.active[target] = false;
215        self.active_count -= 1;
216        let mut target_group = core::mem::take(&mut self.groups[target]);
217        self.groups[source].append(&mut target_group);
218        self.groups[source].sort_unstable();
219        Ok(())
220    }
221}
222
223fn pop_current<M: Measure>(
224    queue: &mut MaxQueue<M>,
225    active: &[bool],
226    added: &[bool],
227    weights: &[M],
228) -> (usize, M) {
229    while let Some((node, weight)) = queue.pop() {
230        if active[node] && !added[node] && weight.compare(weights[node]) == Some(Ordering::Equal) {
231            return (node, weight);
232        }
233    }
234    unreachable!("every active node is queued")
235}