problemreductions/models/graph/
max_cut.rs

1//! MaxCut problem implementation.
2//!
3//! The Maximum Cut problem asks for a partition of vertices into two sets
4//! that maximizes the total weight of edges crossing the partition.
5
6use crate::graph_types::SimpleGraph;
7use crate::traits::Problem;
8use crate::types::{EnergyMode, ProblemSize, SolutionSize};
9use petgraph::graph::{NodeIndex, UnGraph};
10use petgraph::visit::EdgeRef;
11use serde::{Deserialize, Serialize};
12
13/// The Maximum Cut problem.
14///
15/// Given a weighted graph G = (V, E) with edge weights w_e,
16/// find a partition of V into sets S and V\S such that
17/// the total weight of edges crossing the cut is maximized.
18///
19/// # Representation
20///
21/// Each vertex is assigned a binary value:
22/// - 0: vertex is in set S
23/// - 1: vertex is in set V\S
24///
25/// An edge contributes to the cut if its endpoints are in different sets.
26///
27/// # Example
28///
29/// ```
30/// use problemreductions::models::graph::MaxCut;
31/// use problemreductions::{Problem, Solver, BruteForce};
32///
33/// // Create a triangle with unit weights
34/// let problem = MaxCut::new(3, vec![(0, 1, 1), (1, 2, 1), (0, 2, 1)]);
35///
36/// // Solve with brute force
37/// let solver = BruteForce::new();
38/// let solutions = solver.find_best(&problem);
39///
40/// // Maximum cut in triangle is 2 (any partition cuts 2 edges)
41/// for sol in solutions {
42///     let size = problem.solution_size(&sol);
43///     assert_eq!(size.size, 2);
44/// }
45/// ```
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct MaxCut<W = i32> {
48    /// The underlying weighted graph.
49    graph: UnGraph<(), W>,
50}
51
52impl<W: Clone + Default> MaxCut<W> {
53    /// Create a new MaxCut problem.
54    ///
55    /// # Arguments
56    /// * `num_vertices` - Number of vertices
57    /// * `edges` - List of weighted edges as (u, v, weight) triples
58    pub fn new(num_vertices: usize, edges: Vec<(usize, usize, W)>) -> Self {
59        let mut graph = UnGraph::new_undirected();
60        for _ in 0..num_vertices {
61            graph.add_node(());
62        }
63        for (u, v, w) in edges {
64            graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), w);
65        }
66        Self { graph }
67    }
68
69    /// Create a MaxCut problem with unit weights.
70    pub fn unweighted(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self
71    where
72        W: From<i32>,
73    {
74        Self::new(
75            num_vertices,
76            edges.into_iter().map(|(u, v)| (u, v, W::from(1))).collect(),
77        )
78    }
79
80    /// Get the number of vertices.
81    pub fn num_vertices(&self) -> usize {
82        self.graph.node_count()
83    }
84
85    /// Get the number of edges.
86    pub fn num_edges(&self) -> usize {
87        self.graph.edge_count()
88    }
89
90    /// Get the edges with weights.
91    pub fn edges(&self) -> Vec<(usize, usize, W)> {
92        self.graph
93            .edge_references()
94            .map(|e| (e.source().index(), e.target().index(), e.weight().clone()))
95            .collect()
96    }
97
98    /// Get the weight of an edge.
99    pub fn edge_weight(&self, u: usize, v: usize) -> Option<&W> {
100        self.graph
101            .find_edge(NodeIndex::new(u), NodeIndex::new(v))
102            .map(|e| self.graph.edge_weight(e).unwrap())
103    }
104
105    /// Create a MaxCut problem from edges without weights in tuple form.
106    pub fn with_weights(num_vertices: usize, edges: Vec<(usize, usize)>, weights: Vec<W>) -> Self {
107        assert_eq!(edges.len(), weights.len(), "edges and weights must have same length");
108        let mut graph = UnGraph::new_undirected();
109        for _ in 0..num_vertices {
110            graph.add_node(());
111        }
112        for ((u, v), w) in edges.into_iter().zip(weights.into_iter()) {
113            graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), w);
114        }
115        Self { graph }
116    }
117
118    /// Get edge weights only.
119    pub fn edge_weights(&self) -> Vec<W> {
120        self.graph
121            .edge_references()
122            .map(|e| e.weight().clone())
123            .collect()
124    }
125}
126
127impl<W> Problem for MaxCut<W>
128where
129    W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static,
130{
131    const NAME: &'static str = "MaxCut";
132    type GraphType = SimpleGraph;
133    type Weight = W;
134    type Size = W;
135
136    fn num_variables(&self) -> usize {
137        self.graph.node_count()
138    }
139
140    fn num_flavors(&self) -> usize {
141        2 // Binary partition
142    }
143
144    fn problem_size(&self) -> ProblemSize {
145        ProblemSize::new(vec![
146            ("num_vertices", self.graph.node_count()),
147            ("num_edges", self.graph.edge_count()),
148        ])
149    }
150
151    fn energy_mode(&self) -> EnergyMode {
152        EnergyMode::LargerSizeIsBetter // Maximize cut weight
153    }
154
155    fn solution_size(&self, config: &[usize]) -> SolutionSize<Self::Size> {
156        let cut_weight = compute_cut_weight(&self.graph, config);
157        // MaxCut is always valid (any partition is allowed)
158        SolutionSize::valid(cut_weight)
159    }
160}
161
162/// Compute the total weight of edges crossing the cut.
163fn compute_cut_weight<W>(graph: &UnGraph<(), W>, config: &[usize]) -> W
164where
165    W: Clone + num_traits::Zero + std::ops::AddAssign,
166{
167    let mut total = W::zero();
168    for edge in graph.edge_references() {
169        let u = edge.source().index();
170        let v = edge.target().index();
171        let u_side = config.get(u).copied().unwrap_or(0);
172        let v_side = config.get(v).copied().unwrap_or(0);
173        if u_side != v_side {
174            total += edge.weight().clone();
175        }
176    }
177    total
178}
179
180/// Compute the cut size for a given partition.
181///
182/// # Arguments
183/// * `edges` - List of weighted edges as (u, v, weight) triples
184/// * `partition` - Boolean slice indicating which set each vertex belongs to
185pub fn cut_size<W>(edges: &[(usize, usize, W)], partition: &[bool]) -> W
186where
187    W: Clone + num_traits::Zero + std::ops::AddAssign,
188{
189    let mut total = W::zero();
190    for (u, v, w) in edges {
191        if *u < partition.len() && *v < partition.len() && partition[*u] != partition[*v] {
192            total += w.clone();
193        }
194    }
195    total
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::solvers::{BruteForce, Solver};
202
203    #[test]
204    fn test_maxcut_creation() {
205        let problem = MaxCut::new(4, vec![(0, 1, 1), (1, 2, 2), (2, 3, 3)]);
206        assert_eq!(problem.num_vertices(), 4);
207        assert_eq!(problem.num_edges(), 3);
208        assert_eq!(problem.num_variables(), 4);
209        assert_eq!(problem.num_flavors(), 2);
210    }
211
212    #[test]
213    fn test_maxcut_unweighted() {
214        let problem = MaxCut::<i32>::unweighted(3, vec![(0, 1), (1, 2)]);
215        assert_eq!(problem.num_edges(), 2);
216    }
217
218    #[test]
219    fn test_solution_size() {
220        let problem = MaxCut::new(3, vec![(0, 1, 1), (1, 2, 2), (0, 2, 3)]);
221
222        // All same partition: no cut
223        let sol = problem.solution_size(&[0, 0, 0]);
224        assert_eq!(sol.size, 0);
225        assert!(sol.is_valid);
226
227        // 0 vs {1,2}: cuts edges 0-1 (1) and 0-2 (3) = 4
228        let sol = problem.solution_size(&[0, 1, 1]);
229        assert_eq!(sol.size, 4);
230
231        // {0,2} vs {1}: cuts edges 0-1 (1) and 1-2 (2) = 3
232        let sol = problem.solution_size(&[0, 1, 0]);
233        assert_eq!(sol.size, 3);
234    }
235
236    #[test]
237    fn test_brute_force_triangle() {
238        // Triangle with unit weights: max cut is 2
239        let problem = MaxCut::<i32>::unweighted(3, vec![(0, 1), (1, 2), (0, 2)]);
240        let solver = BruteForce::new();
241
242        let solutions = solver.find_best(&problem);
243        for sol in &solutions {
244            let size = problem.solution_size(sol);
245            assert_eq!(size.size, 2);
246        }
247    }
248
249    #[test]
250    fn test_brute_force_path() {
251        // Path 0-1-2: max cut is 2 (partition {0,2} vs {1})
252        let problem = MaxCut::<i32>::unweighted(3, vec![(0, 1), (1, 2)]);
253        let solver = BruteForce::new();
254
255        let solutions = solver.find_best(&problem);
256        for sol in &solutions {
257            let size = problem.solution_size(sol);
258            assert_eq!(size.size, 2);
259        }
260    }
261
262    #[test]
263    fn test_brute_force_weighted() {
264        // Edge with weight 10 should always be cut
265        let problem = MaxCut::new(3, vec![(0, 1, 10), (1, 2, 1)]);
266        let solver = BruteForce::new();
267
268        let solutions = solver.find_best(&problem);
269        // Max is 11 (cut both edges) with partition like [0,1,0] or [1,0,1]
270        for sol in &solutions {
271            let size = problem.solution_size(sol);
272            assert_eq!(size.size, 11);
273        }
274    }
275
276    #[test]
277    fn test_cut_size_function() {
278        let edges = vec![(0, 1, 1), (1, 2, 2), (0, 2, 3)];
279
280        // Partition {0} vs {1, 2}
281        assert_eq!(cut_size(&edges, &[false, true, true]), 4); // 1 + 3
282
283        // Partition {0, 1} vs {2}
284        assert_eq!(cut_size(&edges, &[false, false, true]), 5); // 2 + 3
285
286        // All same partition
287        assert_eq!(cut_size(&edges, &[false, false, false]), 0);
288    }
289
290    #[test]
291    fn test_edge_weight() {
292        let problem = MaxCut::new(3, vec![(0, 1, 5), (1, 2, 10)]);
293        assert_eq!(problem.edge_weight(0, 1), Some(&5));
294        assert_eq!(problem.edge_weight(1, 2), Some(&10));
295        assert_eq!(problem.edge_weight(0, 2), None);
296    }
297
298    #[test]
299    fn test_edges() {
300        let problem = MaxCut::new(3, vec![(0, 1, 1), (1, 2, 2)]);
301        let edges = problem.edges();
302        assert_eq!(edges.len(), 2);
303    }
304
305    #[test]
306    fn test_energy_mode() {
307        let problem = MaxCut::<i32>::unweighted(2, vec![(0, 1)]);
308        assert!(problem.energy_mode().is_maximization());
309    }
310
311    #[test]
312    fn test_empty_graph() {
313        let problem = MaxCut::<i32>::unweighted(3, vec![]);
314        let solver = BruteForce::new();
315
316        let solutions = solver.find_best(&problem);
317        // Any partition gives cut size 0
318        assert!(!solutions.is_empty());
319        for sol in &solutions {
320            assert_eq!(problem.solution_size(sol).size, 0);
321        }
322    }
323
324    #[test]
325    fn test_single_edge() {
326        let problem = MaxCut::new(2, vec![(0, 1, 5)]);
327        let solver = BruteForce::new();
328
329        let solutions = solver.find_best(&problem);
330        // Putting vertices in different sets maximizes cut
331        assert_eq!(solutions.len(), 2); // [0,1] and [1,0]
332        for sol in &solutions {
333            assert_eq!(problem.solution_size(sol).size, 5);
334        }
335    }
336
337    #[test]
338    fn test_complete_graph_k4() {
339        // K4: every partition cuts exactly 4 edges (balanced) or less
340        let problem =
341            MaxCut::<i32>::unweighted(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
342        let solver = BruteForce::new();
343
344        let solutions = solver.find_best(&problem);
345        // Max cut in K4 is 4 (2-2 partition)
346        for sol in &solutions {
347            assert_eq!(problem.solution_size(sol).size, 4);
348        }
349    }
350
351    #[test]
352    fn test_bipartite_graph() {
353        // Complete bipartite K_{2,2}: max cut is all 4 edges
354        let problem = MaxCut::<i32>::unweighted(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)]);
355        let solver = BruteForce::new();
356
357        let solutions = solver.find_best(&problem);
358        // Bipartite graph can achieve max cut = all edges
359        for sol in &solutions {
360            assert_eq!(problem.solution_size(sol).size, 4);
361        }
362    }
363
364    #[test]
365    fn test_symmetry() {
366        // Complementary partitions should give same cut
367        let problem = MaxCut::<i32>::unweighted(3, vec![(0, 1), (1, 2), (0, 2)]);
368
369        let sol1 = problem.solution_size(&[0, 1, 1]);
370        let sol2 = problem.solution_size(&[1, 0, 0]); // complement
371        assert_eq!(sol1.size, sol2.size);
372    }
373}