Skip to main content

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::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Max, One, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "MaxCut",
16        display_name: "Max Cut",
17        aliases: &["MaximumBipartiteSubgraph"],
18        dimensions: &[
19            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
20            VariantDimension::new("weight", "i64", &["i64", "One"]),
21        ],
22        category: crate::registry::ProblemCategory::Graph,
23        module_path: module_path!(),
24        description: "Find maximum weight cut in a graph",
25        fields: MaxCutI64CreateSpec::FIELDS,
26    }
27}
28
29/// The Maximum Cut problem.
30///
31/// Given a weighted graph G = (V, E) with edge weights w_e,
32/// find a partition of V into sets S and V\S such that
33/// the total weight of edges crossing the cut is maximized.
34///
35/// # Representation
36///
37/// Each vertex is assigned a binary value:
38/// - 0: vertex is in set S
39/// - 1: vertex is in set V\S
40///
41/// An edge contributes to the cut if its endpoints are in different sets.
42///
43/// # Type Parameters
44///
45/// * `G` - The graph type (e.g., `SimpleGraph`, `KingsSubgraph`, `UnitDiskGraph`)
46/// * `W` - The weight type for edges (e.g., `i64`, `f64`)
47///
48/// # Example
49///
50/// ```
51/// use problemreductions::models::graph::MaxCut;
52/// use problemreductions::topology::SimpleGraph;
53/// use problemreductions::types::Max;
54/// use problemreductions::{Problem, BruteForce};
55///
56/// // Create a triangle with unit weights
57/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]);
58/// let problem = MaxCut::new(graph, vec![1, 1, 1]);
59///
60/// // Solve with brute force
61/// let solver = BruteForce::new();
62/// let solutions = solver.find_all_witnesses(&problem).unwrap();
63///
64/// // Maximum cut in triangle is 2 (any partition cuts 2 edges)
65/// for sol in solutions {
66///     let size = problem.evaluate(&sol).unwrap();
67///     assert_eq!(size, Max(Some(2)));
68/// }
69/// ```
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct MaxCut<G, W> {
72    /// The underlying graph structure.
73    graph: G,
74    /// Weights for each edge (in the same order as graph.edges()).
75    edge_weights: Vec<W>,
76}
77
78macro_rules! max_cut_create_spec {
79    ($name:ident, $weight:ty, $one:expr $(, $edge_weights:ident)?) => {
80        #[derive(Debug, Deserialize, crate::CreateSpec)]
81        struct $name {
82            #[create(codec = "edge-list")]
83            graph: Vec<(usize, usize)>,
84            num_vertices: Option<usize>,
85            $(
86            #[create(codec = "comma-separated")]
87            $edge_weights: Option<Vec<$weight>>,
88            )?
89        }
90
91        impl TryFrom<$name> for MaxCut<SimpleGraph, $weight> {
92            type Error = crate::registry::ConstructionError;
93
94            fn try_from(spec: $name) -> Result<Self, Self::Error> {
95                let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
96                let edge_weights = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; graph.num_edges()] } };
97                if edge_weights.len() != graph.num_edges() {
98                    return Err(format!(
99                        "edge_weights has length {}, expected {}",
100                        edge_weights.len(),
101                        graph.num_edges()
102                    )
103                    .into());
104                }
105                Ok(Self::new(graph, edge_weights))
106            }
107        }
108    };
109}
110
111max_cut_create_spec!(MaxCutI64CreateSpec, i64, 1, edge_weights);
112max_cut_create_spec!(MaxCutOneCreateSpec, One, One);
113
114fn simple_graph_from_create(
115    edges: Vec<(usize, usize)>,
116    num_vertices: Option<usize>,
117) -> Result<SimpleGraph, crate::registry::ConstructionError> {
118    if edges.is_empty() && num_vertices.is_none() {
119        return Err("num_vertices is required for an empty graph"
120            .to_string()
121            .into());
122    }
123    for (index, &(u, v)) in edges.iter().enumerate() {
124        if u == v {
125            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
126        }
127    }
128    let inferred = edges
129        .iter()
130        .flat_map(|&(u, v)| [u, v])
131        .max()
132        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
133        .transpose()?
134        .unwrap_or(0);
135    let num_vertices = num_vertices.unwrap_or(inferred);
136    if num_vertices < inferred {
137        return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into());
138    }
139    Ok(SimpleGraph::new(num_vertices, edges))
140}
141
142impl<G: Graph, W: Clone + Default> MaxCut<G, W> {
143    /// Create a MaxCut problem from a graph with specified edge weights.
144    ///
145    /// # Arguments
146    /// * `graph` - The underlying graph
147    /// * `edge_weights` - Weights for each edge (must match graph.num_edges())
148    pub fn new(graph: G, edge_weights: Vec<W>) -> Self {
149        assert_eq!(
150            edge_weights.len(),
151            graph.num_edges(),
152            "edge_weights length must match num_edges"
153        );
154        Self {
155            graph,
156            edge_weights,
157        }
158    }
159
160    /// Create a MaxCut problem with unit weights.
161    pub fn unweighted(graph: G) -> Self
162    where
163        W: WeightElement,
164    {
165        let edge_weights = vec![W::unit(); graph.num_edges()];
166        Self {
167            graph,
168            edge_weights,
169        }
170    }
171
172    /// Get a reference to the underlying graph.
173    pub fn graph(&self) -> &G {
174        &self.graph
175    }
176
177    /// Get the edges with weights.
178    pub fn edges(&self) -> Vec<(usize, usize, W)> {
179        self.graph
180            .edges()
181            .into_iter()
182            .zip(self.edge_weights.iter())
183            .map(|((u, v), w)| (u, v, w.clone()))
184            .collect()
185    }
186
187    /// Get the weight of an edge by its index.
188    pub fn edge_weight_by_index(&self, idx: usize) -> Option<&W> {
189        self.edge_weights.get(idx)
190    }
191
192    /// Get the weight of an edge between vertices u and v.
193    pub fn edge_weight(&self, u: usize, v: usize) -> Option<&W> {
194        // Find the edge index
195        for (idx, (eu, ev)) in self.graph.edges().iter().enumerate() {
196            if (*eu == u && *ev == v) || (*eu == v && *ev == u) {
197                return self.edge_weights.get(idx);
198            }
199        }
200        None
201    }
202
203    /// Get edge weights only.
204    pub fn edge_weights(&self) -> Vec<W> {
205        self.edge_weights.clone()
206    }
207
208    /// Compute the cut size for a given partition configuration.
209    pub fn cut_size(&self, config: &[bool]) -> Result<W::Sum, crate::traits::EvaluationError>
210    where
211        W: WeightElement,
212    {
213        cut_size(&self.graph, &self.edge_weights, config)
214    }
215}
216
217impl<G: Graph, W: WeightElement> MaxCut<G, W> {
218    /// Get the number of vertices in the underlying graph.
219    pub fn num_vertices(&self) -> usize {
220        self.graph().num_vertices()
221    }
222
223    /// Get the number of edges in the underlying graph.
224    pub fn num_edges(&self) -> usize {
225        self.graph().num_edges()
226    }
227}
228
229impl<G, W> Problem for MaxCut<G, W>
230where
231    G: Graph + crate::variant::VariantParam,
232    W: WeightElement + crate::variant::VariantParam,
233{
234    const NAME: &'static str = "MaxCut";
235    type Solution = Vec<bool>;
236    type Value = Max<W::Sum>;
237
238    crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
239
240    fn variant() -> Vec<(&'static str, &'static str)> {
241        crate::variant_params![G, W]
242    }
243
244    fn evaluate(
245        &self,
246        config: &Self::Solution,
247    ) -> Result<Max<W::Sum>, crate::traits::EvaluationError> {
248        if config.len() != self.graph.num_vertices() {
249            return Err(crate::traits::EvaluationError::InvalidConfiguration(
250                "cut assignment length does not match the graph vertices".into(),
251            ));
252        }
253        Ok({
254            // All cuts are valid, so always return Valid
255            Max(Some(cut_size(&self.graph, &self.edge_weights, config)?))
256        })
257    }
258}
259
260impl<G, W> crate::solvers::BruteForceProblem for MaxCut<G, W>
261where
262    G: Graph + crate::variant::VariantParam,
263    W: WeightElement + crate::variant::VariantParam,
264{
265    fn dimensions(&self) -> Vec<usize> {
266        vec![2; self.graph.num_vertices()]
267    }
268}
269
270/// Compute the total weight of edges crossing the cut.
271///
272/// # Arguments
273/// * `graph` - The graph structure
274/// * `edge_weights` - Weights for each edge (same order as `graph.edges()`)
275/// * `partition` - Boolean slice indicating which set each vertex belongs to
276pub(crate) fn cut_size<G, W>(
277    graph: &G,
278    edge_weights: &[W],
279    partition: &[bool],
280) -> Result<W::Sum, crate::traits::EvaluationError>
281where
282    G: Graph,
283    W: WeightElement,
284{
285    let mut total = W::Sum::zero();
286    for ((u, v), weight) in graph.edges().iter().zip(edge_weights.iter()) {
287        if *u < partition.len() && *v < partition.len() && partition[*u] != partition[*v] {
288            total = W::checked_add_to_sum(total, weight.to_sum(), "summing cut-edge weights")?;
289        }
290    }
291    Ok(total)
292}
293
294crate::impl_random_generate!(MaxCut<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
295    let graph = spec.graph()?;
296    let weights = vec![1; graph.num_edges()];
297    Ok(MaxCut::new(graph, weights))
298});
299
300crate::declare_variants! {
301    default MaxCut<SimpleGraph, i64> => "2^(2.372 * num_vertices / 3)" create MaxCutI64CreateSpec random,
302    MaxCut<SimpleGraph, One> => "2^(0.7907 * num_vertices)" create MaxCutOneCreateSpec,
303}
304
305crate::register_brute_force! {
306    MaxCut<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
307    MaxCut<SimpleGraph, One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
308}
309
310#[cfg(feature = "example-db")]
311pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
312    vec![
313        crate::example_db::specs::ModelExampleSpec {
314            id: "max_cut_simplegraph",
315            instance: Box::new(MaxCut::<_, i64>::unweighted(SimpleGraph::new(
316                5,
317                vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)],
318            ))),
319            optimal_config: serde_json::json!(vec![true, false, false, true, false]),
320            optimal_value: serde_json::json!(5),
321        },
322        crate::example_db::specs::ModelExampleSpec {
323            id: "max_cut_seven_edge_graph",
324            instance: Box::new(MaxCut::new(
325                SimpleGraph::new(
326                    5,
327                    vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)],
328                ),
329                vec![One; 7],
330            )),
331            optimal_config: serde_json::json!(vec![false, true, false, true, false]),
332            optimal_value: serde_json::json!(6),
333        },
334    ]
335}
336
337#[cfg(test)]
338#[path = "../../unit_tests/models/graph/max_cut.rs"]
339mod tests;