problemreductions/models/graph/
maximum_matching.rs1use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Max, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "MaximumMatching",
17 display_name: "Maximum Matching",
18 aliases: &["MaxMatching"],
19 dimensions: &[
20 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
21 VariantDimension::new("weight", "i64", &["i64"]),
22 ],
23 category: crate::registry::ProblemCategory::Graph,
24 module_path: module_path!(),
25 description: "Find maximum weight matching in a graph",
26 fields: MaximumMatchingCreateSpec::FIELDS,
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct MaximumMatching<G, W> {
61 graph: G,
63 edge_weights: Vec<W>,
65}
66
67#[derive(Debug, Deserialize, crate::CreateSpec)]
68struct MaximumMatchingCreateSpec {
69 #[create(codec = "edge-list")]
70 graph: Vec<(usize, usize)>,
71 num_vertices: Option<usize>,
72 #[create(codec = "comma-separated")]
73 edge_weights: Option<Vec<i64>>,
74}
75
76impl TryFrom<MaximumMatchingCreateSpec> for MaximumMatching<SimpleGraph, i64> {
77 type Error = crate::registry::ConstructionError;
78
79 fn try_from(spec: MaximumMatchingCreateSpec) -> Result<Self, Self::Error> {
80 let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
81 let edge_weights = spec
82 .edge_weights
83 .unwrap_or_else(|| vec![1; graph.num_edges()]);
84 if edge_weights.len() != graph.num_edges() {
85 return Err(format!(
86 "edge_weights has length {}, expected {}",
87 edge_weights.len(),
88 graph.num_edges()
89 )
90 .into());
91 }
92 Ok(Self::new(graph, edge_weights))
93 }
94}
95
96fn simple_graph_from_create(
97 edges: Vec<(usize, usize)>,
98 num_vertices: Option<usize>,
99) -> Result<SimpleGraph, crate::registry::ConstructionError> {
100 if edges.is_empty() && num_vertices.is_none() {
101 return Err("num_vertices is required for an empty graph"
102 .to_string()
103 .into());
104 }
105 for (index, &(u, v)) in edges.iter().enumerate() {
106 if u == v {
107 return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
108 }
109 }
110 let inferred = edges
111 .iter()
112 .flat_map(|&(u, v)| [u, v])
113 .max()
114 .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
115 .transpose()?
116 .unwrap_or(0);
117 let num_vertices = num_vertices.unwrap_or(inferred);
118 if num_vertices < inferred {
119 return Err(format!(
120 "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
121 )
122 .into());
123 }
124 Ok(SimpleGraph::new(num_vertices, edges))
125}
126
127impl<G: Graph, W: Clone + Default> MaximumMatching<G, W> {
128 pub fn new(graph: G, edge_weights: Vec<W>) -> Self {
134 assert_eq!(
135 edge_weights.len(),
136 graph.num_edges(),
137 "edge_weights length must match num_edges"
138 );
139 Self {
140 graph,
141 edge_weights,
142 }
143 }
144
145 pub fn unit_weights(graph: G) -> Self
147 where
148 W: WeightElement,
149 {
150 let edge_weights = vec![W::unit(); graph.num_edges()];
151 Self {
152 graph,
153 edge_weights,
154 }
155 }
156
157 pub fn graph(&self) -> &G {
159 &self.graph
160 }
161
162 pub fn edge_endpoints(&self, edge_idx: usize) -> Option<(usize, usize)> {
164 self.graph.edges().get(edge_idx).copied()
165 }
166
167 pub fn edges(&self) -> Vec<(usize, usize, W)> {
169 self.graph
170 .edges()
171 .into_iter()
172 .zip(self.edge_weights.iter().cloned())
173 .map(|((u, v), w)| (u, v, w))
174 .collect()
175 }
176
177 pub fn vertex_to_edges(&self) -> HashMap<usize, Vec<usize>> {
179 let mut v2e: HashMap<usize, Vec<usize>> = HashMap::new();
180 for (idx, (u, v)) in self.graph.edges().iter().enumerate() {
181 v2e.entry(*u).or_default().push(idx);
182 v2e.entry(*v).or_default().push(idx);
183 }
184 v2e
185 }
186
187 pub fn is_valid_solution(&self, config: &[bool]) -> bool {
189 self.is_valid_matching(config)
190 }
191
192 fn is_valid_matching(&self, config: &[bool]) -> bool {
194 let mut vertex_used = vec![false; self.graph.num_vertices()];
195
196 for (idx, &selected) in config.iter().enumerate() {
197 if selected {
198 if let Some((u, v)) = self.edge_endpoints(idx) {
199 if vertex_used[u] || vertex_used[v] {
200 return false;
201 }
202 vertex_used[u] = true;
203 vertex_used[v] = true;
204 }
205 }
206 }
207 true
208 }
209
210 pub fn set_weights(&mut self, weights: Vec<W>) {
212 assert_eq!(weights.len(), self.graph.num_edges());
213 self.edge_weights = weights;
214 }
215
216 pub fn weights(&self) -> Vec<W> {
218 self.edge_weights.clone()
219 }
220
221 pub fn is_weighted(&self) -> bool
223 where
224 W: WeightElement,
225 {
226 !W::IS_UNIT
227 }
228}
229
230impl<G: Graph, W: WeightElement> MaximumMatching<G, W> {
231 pub fn num_vertices(&self) -> usize {
233 self.graph().num_vertices()
234 }
235
236 pub fn num_edges(&self) -> usize {
238 self.graph().num_edges()
239 }
240}
241
242impl<G, W> Problem for MaximumMatching<G, W>
243where
244 G: Graph + crate::variant::VariantParam,
245 W: WeightElement + crate::variant::VariantParam,
246{
247 const NAME: &'static str = "MaximumMatching";
248 type Solution = Vec<bool>;
249 type Value = Max<W::Sum>;
250
251 crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
252
253 fn variant() -> Vec<(&'static str, &'static str)> {
254 crate::variant_params![G, W]
255 }
256
257 fn evaluate(
258 &self,
259 config: &Self::Solution,
260 ) -> Result<Max<W::Sum>, crate::traits::EvaluationError> {
261 if config.len() != self.graph.num_edges() {
262 return Err(crate::traits::EvaluationError::InvalidConfiguration(
263 "edge-selection length does not match the graph".into(),
264 ));
265 }
266 Ok({
267 if !self.is_valid_matching(config) {
268 return Ok(Max(None));
269 }
270 let mut total = W::Sum::zero();
271 for (idx, &selected) in config.iter().enumerate() {
272 if selected {
273 if let Some(w) = self.edge_weights.get(idx) {
274 total = W::checked_add_to_sum(
275 total,
276 w.to_sum(),
277 "summing selected matching-edge weights",
278 )?;
279 }
280 }
281 }
282 Max(Some(total))
283 })
284 }
285}
286
287impl<G, W> crate::solvers::BruteForceProblem for MaximumMatching<G, W>
288where
289 G: Graph + crate::variant::VariantParam,
290 W: WeightElement + crate::variant::VariantParam,
291{
292 fn dimensions(&self) -> Vec<usize> {
293 vec![2; self.graph.num_edges()]
294 }
295}
296
297crate::impl_random_generate!(MaximumMatching<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
298 let graph = spec.graph()?;
299 let weights = vec![1; graph.num_edges()];
300 Ok(MaximumMatching::new(graph, weights))
301});
302
303crate::declare_variants! {
304 default MaximumMatching<SimpleGraph, i64> => "num_vertices^3" create MaximumMatchingCreateSpec random,
305}
306
307crate::register_brute_force! {
308 MaximumMatching<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
309}
310
311#[cfg(feature = "example-db")]
312pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
313 vec![crate::example_db::specs::ModelExampleSpec {
314 id: "maximum_matching_simplegraph",
315 instance: Box::new(MaximumMatching::<_, i64>::unit_weights(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, false, true, false]),
320 optimal_value: serde_json::json!(2),
321 }]
322}
323
324#[cfg(test)]
329pub(crate) fn is_matching<G: Graph>(graph: &G, selected: &[bool]) -> bool {
330 assert_eq!(
331 selected.len(),
332 graph.num_edges(),
333 "selected length must match num_edges"
334 );
335
336 let edges = graph.edges();
337 let mut vertex_used = vec![false; graph.num_vertices()];
338 for (idx, &sel) in selected.iter().enumerate() {
339 if sel {
340 let (u, v) = edges[idx];
341 if vertex_used[u] || vertex_used[v] {
342 return false;
343 }
344 vertex_used[u] = true;
345 vertex_used[v] = true;
346 }
347 }
348 true
349}
350
351#[cfg(test)]
352#[path = "../../unit_tests/models/graph/maximum_matching.rs"]
353mod tests;