Skip to main content

rust_igraph/algorithms/
max_cut.rs

1//! Maximum cut algorithms (ALGO-FL-002).
2//!
3//! A cut of a graph partitions the vertices into two disjoint sets
4//! (S, V\S). The cut value is the number (or total weight) of edges
5//! crossing the partition. Finding a maximum cut is NP-hard; we
6//! provide a greedy 0.5-approximation.
7//!
8//! The greedy algorithm processes vertices in order, assigning each
9//! to the side that maximizes the number of crossing edges. This
10//! guarantees a cut of at least |E|/2.
11
12use crate::core::{Graph, IgraphResult, VertexId};
13
14/// Result of [`maximum_cut`].
15#[derive(Debug, Clone)]
16pub struct MaxCutResult {
17    /// The partition: `partition[v]` is `true` if vertex `v` is in
18    /// set S, `false` if in set V\S.
19    pub partition: Vec<bool>,
20    /// Number of edges crossing the cut.
21    pub cut_value: usize,
22}
23
24/// Compute the cut value for a given vertex partition.
25///
26/// Returns the number of edges with exactly one endpoint in each
27/// side of the partition. For directed graphs, counts edges
28/// crossing in either direction.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, cut_value};
34///
35/// let mut g = Graph::with_vertices(4);
36/// g.add_edge(0, 1).unwrap();
37/// g.add_edge(1, 2).unwrap();
38/// g.add_edge(2, 3).unwrap();
39/// g.add_edge(0, 3).unwrap();
40///
41/// // Partition {0,1} vs {2,3}: edges 1-2 and 0-3 cross → value = 2
42/// let partition = vec![true, true, false, false];
43/// assert_eq!(cut_value(&g, &partition).unwrap(), 2);
44/// ```
45#[allow(clippy::cast_possible_truncation)]
46pub fn cut_value(graph: &Graph, partition: &[bool]) -> IgraphResult<usize> {
47    let ecount = graph.ecount();
48    let mut value = 0usize;
49
50    for eid in 0..ecount as u32 {
51        let (u, v) = graph.edge(eid)?;
52        if partition[u as usize] != partition[v as usize] {
53            value = value.saturating_add(1);
54        }
55    }
56
57    Ok(value)
58}
59
60/// Compute an approximate maximum cut using a greedy heuristic.
61///
62/// Processes vertices in order. Each vertex is assigned to the side
63/// (S or V\S) that maximizes the number of edges crossing the cut.
64/// The result is guaranteed to have a cut value of at least |E|/2.
65///
66/// Self-loops never cross a cut (both endpoints are the same vertex).
67///
68/// # Examples
69///
70/// ```
71/// use rust_igraph::{Graph, maximum_cut, cut_value};
72///
73/// // Complete graph K4: max cut is 4 (partition {0,2} vs {1,3})
74/// let mut g = Graph::with_vertices(4);
75/// g.add_edge(0, 1).unwrap();
76/// g.add_edge(0, 2).unwrap();
77/// g.add_edge(0, 3).unwrap();
78/// g.add_edge(1, 2).unwrap();
79/// g.add_edge(1, 3).unwrap();
80/// g.add_edge(2, 3).unwrap();
81/// let result = maximum_cut(&g).unwrap();
82/// assert!(result.cut_value >= 3); // at least |E|/2 = 3
83/// assert_eq!(result.cut_value, cut_value(&g, &result.partition).unwrap());
84/// ```
85#[allow(clippy::cast_possible_truncation)]
86pub fn maximum_cut(graph: &Graph) -> IgraphResult<MaxCutResult> {
87    let n = graph.vcount();
88    let directed = graph.is_directed();
89
90    if n == 0 {
91        return Ok(MaxCutResult {
92            partition: Vec::new(),
93            cut_value: 0,
94        });
95    }
96
97    let mut partition = vec![false; n as usize];
98
99    // Precompute adjacency: for each vertex, its neighbors (both directions for directed)
100    let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n as usize);
101    for v in 0..n {
102        let mut neighbors = graph.neighbors(v)?;
103        if directed {
104            let in_n = graph.in_neighbors_vec(v)?;
105            for w in in_n {
106                if !neighbors.contains(&w) {
107                    neighbors.push(w);
108                }
109            }
110        }
111        adj.push(neighbors);
112    }
113
114    // Greedy: for each vertex, count how many already-placed neighbors
115    // are in S vs V\S. Place vertex on the side with fewer neighbors
116    // (to maximize crossing edges).
117    for v in 0..n {
118        let mut count_in_s = 0u32;
119        let mut count_in_complement = 0u32;
120
121        for &w in &adj[v as usize] {
122            if w == v || w >= v {
123                continue;
124            }
125            if partition[w as usize] {
126                count_in_s = count_in_s.saturating_add(1);
127            } else {
128                count_in_complement = count_in_complement.saturating_add(1);
129            }
130        }
131
132        partition[v as usize] = count_in_complement > count_in_s;
133    }
134
135    let value = cut_value(graph, &partition)?;
136
137    Ok(MaxCutResult {
138        partition,
139        cut_value: value,
140    })
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn empty_graph() {
149        let g = Graph::with_vertices(0);
150        let r = maximum_cut(&g).unwrap();
151        assert!(r.partition.is_empty());
152        assert_eq!(r.cut_value, 0);
153    }
154
155    #[test]
156    fn no_edges() {
157        let g = Graph::with_vertices(5);
158        let r = maximum_cut(&g).unwrap();
159        assert_eq!(r.cut_value, 0);
160    }
161
162    #[test]
163    fn single_edge() {
164        let mut g = Graph::with_vertices(2);
165        g.add_edge(0, 1).unwrap();
166        let r = maximum_cut(&g).unwrap();
167        assert_eq!(r.cut_value, 1);
168        assert_ne!(r.partition[0], r.partition[1]);
169    }
170
171    #[test]
172    fn path_4() {
173        let mut g = Graph::with_vertices(4);
174        g.add_edge(0, 1).unwrap();
175        g.add_edge(1, 2).unwrap();
176        g.add_edge(2, 3).unwrap();
177        let r = maximum_cut(&g).unwrap();
178        assert_eq!(r.cut_value, cut_value(&g, &r.partition).unwrap());
179        assert!(r.cut_value >= 1); // at least |E|/2 = 1
180    }
181
182    #[test]
183    fn triangle() {
184        let mut g = Graph::with_vertices(3);
185        g.add_edge(0, 1).unwrap();
186        g.add_edge(1, 2).unwrap();
187        g.add_edge(2, 0).unwrap();
188        let r = maximum_cut(&g).unwrap();
189        assert!(r.cut_value >= 1); // at least |E|/2 = 1
190        assert_eq!(r.cut_value, 2); // optimal for triangle
191    }
192
193    #[test]
194    fn complete_k4() {
195        let mut g = Graph::with_vertices(4);
196        for u in 0..4u32 {
197            for v in (u + 1)..4 {
198                g.add_edge(u, v).unwrap();
199            }
200        }
201        let r = maximum_cut(&g).unwrap();
202        assert!(r.cut_value >= 3); // at least |E|/2 = 3
203        assert_eq!(r.cut_value, cut_value(&g, &r.partition).unwrap());
204    }
205
206    #[test]
207    fn bipartite_k22() {
208        let mut g = Graph::with_vertices(4);
209        g.add_edge(0, 2).unwrap();
210        g.add_edge(0, 3).unwrap();
211        g.add_edge(1, 2).unwrap();
212        g.add_edge(1, 3).unwrap();
213        let r = maximum_cut(&g).unwrap();
214        assert!(r.cut_value >= 2); // at least |E|/2 = 2
215        // Bipartite optimal: all 4 edges cross
216        assert_eq!(r.cut_value, 4);
217    }
218
219    #[test]
220    fn star_graph() {
221        let mut g = Graph::with_vertices(5);
222        for i in 1..5u32 {
223            g.add_edge(0, i).unwrap();
224        }
225        let r = maximum_cut(&g).unwrap();
226        assert!(r.cut_value >= 2); // at least |E|/2 = 2
227        assert_eq!(r.cut_value, 4); // star: all 4 edges cross
228    }
229
230    #[test]
231    fn self_loop_ignored() {
232        let mut g = Graph::with_vertices(2);
233        g.add_edge(0, 0).unwrap();
234        g.add_edge(0, 1).unwrap();
235        let r = maximum_cut(&g).unwrap();
236        assert_eq!(r.cut_value, 1); // self-loop doesn't cross
237    }
238
239    #[test]
240    fn directed_graph() {
241        let mut g = Graph::new(3, true).unwrap();
242        g.add_edge(0, 1).unwrap();
243        g.add_edge(1, 2).unwrap();
244        g.add_edge(2, 0).unwrap();
245        let r = maximum_cut(&g).unwrap();
246        assert!(r.cut_value >= 1);
247        assert_eq!(r.cut_value, cut_value(&g, &r.partition).unwrap());
248    }
249
250    #[test]
251    fn cut_value_all_same_side() {
252        let mut g = Graph::with_vertices(3);
253        g.add_edge(0, 1).unwrap();
254        g.add_edge(1, 2).unwrap();
255        let partition = vec![true, true, true];
256        assert_eq!(cut_value(&g, &partition).unwrap(), 0);
257    }
258
259    #[test]
260    fn cut_value_alternating() {
261        let mut g = Graph::with_vertices(4);
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 2).unwrap();
264        g.add_edge(2, 3).unwrap();
265        let partition = vec![true, false, true, false];
266        assert_eq!(cut_value(&g, &partition).unwrap(), 3);
267    }
268
269    #[test]
270    fn guarantee_half_edges() {
271        // Cycle of 6: |E| = 6, guarantee ≥ 3
272        let mut g = Graph::with_vertices(6);
273        for i in 0..6u32 {
274            g.add_edge(i, (i + 1) % 6).unwrap();
275        }
276        let r = maximum_cut(&g).unwrap();
277        assert!(r.cut_value >= 3);
278    }
279
280    #[test]
281    fn parallel_edges() {
282        let mut g = Graph::with_vertices(2);
283        g.add_edge(0, 1).unwrap();
284        g.add_edge(0, 1).unwrap();
285        g.add_edge(0, 1).unwrap();
286        let r = maximum_cut(&g).unwrap();
287        assert_eq!(r.cut_value, 3); // all parallel edges cross
288    }
289}