problemreductions/models/graph/
matching.rs

1//! Matching problem implementation.
2//!
3//! The Maximum Matching problem asks for a maximum weight set of edges
4//! such that no two edges share a vertex.
5
6use crate::graph_types::SimpleGraph;
7use crate::traits::{ConstraintSatisfactionProblem, Problem};
8use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize};
9use petgraph::graph::{NodeIndex, UnGraph};
10use petgraph::visit::EdgeRef;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// The Maximum Matching problem.
15///
16/// Given a graph G = (V, E) with edge weights, find a maximum weight
17/// subset M ⊆ E such that no two edges in M share a vertex.
18///
19/// # Example
20///
21/// ```
22/// use problemreductions::models::graph::Matching;
23/// use problemreductions::{Problem, Solver, BruteForce};
24///
25/// // Path graph 0-1-2
26/// let problem = Matching::new(3, vec![(0, 1, 1), (1, 2, 1)]);
27///
28/// let solver = BruteForce::new();
29/// let solutions = solver.find_best(&problem);
30///
31/// // Maximum matching has 1 edge
32/// for sol in &solutions {
33///     assert_eq!(sol.iter().sum::<usize>(), 1);
34/// }
35/// ```
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Matching<W = i32> {
38    /// Number of vertices.
39    num_vertices: usize,
40    /// The underlying weighted graph.
41    graph: UnGraph<(), W>,
42    /// Weights for each edge (in edge index order).
43    edge_weights: Vec<W>,
44}
45
46impl<W: Clone + Default> Matching<W> {
47    /// Create a new Matching problem.
48    ///
49    /// # Arguments
50    /// * `num_vertices` - Number of vertices
51    /// * `edges` - List of weighted edges as (u, v, weight) triples
52    pub fn new(num_vertices: usize, edges: Vec<(usize, usize, W)>) -> Self {
53        let mut graph = UnGraph::new_undirected();
54        for _ in 0..num_vertices {
55            graph.add_node(());
56        }
57        let mut edge_weights = Vec::new();
58        for (u, v, w) in edges {
59            graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), w.clone());
60            edge_weights.push(w);
61        }
62        Self {
63            num_vertices,
64            graph,
65            edge_weights,
66        }
67    }
68
69    /// Create a Matching 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.num_vertices
83    }
84
85    /// Get the number of edges.
86    pub fn num_edges(&self) -> usize {
87        self.graph.edge_count()
88    }
89
90    /// Get edge endpoints.
91    pub fn edge_endpoints(&self, edge_idx: usize) -> Option<(usize, usize)> {
92        let edge_ref = self.graph.edge_references().nth(edge_idx)?;
93        Some((edge_ref.source().index(), edge_ref.target().index()))
94    }
95
96    /// Get all edges with their endpoints.
97    pub fn edges(&self) -> Vec<(usize, usize, W)> {
98        self.graph
99            .edge_references()
100            .map(|e| (e.source().index(), e.target().index(), e.weight().clone()))
101            .collect()
102    }
103
104    /// Build a map from vertices to incident edges.
105    pub fn vertex_to_edges(&self) -> HashMap<usize, Vec<usize>> {
106        let mut v2e: HashMap<usize, Vec<usize>> = HashMap::new();
107        for (idx, edge) in self.graph.edge_references().enumerate() {
108            let u = edge.source().index();
109            let v = edge.target().index();
110            v2e.entry(u).or_default().push(idx);
111            v2e.entry(v).or_default().push(idx);
112        }
113        v2e
114    }
115
116    /// Check if a configuration is a valid matching.
117    fn is_valid_matching(&self, config: &[usize]) -> bool {
118        let mut vertex_used = vec![false; self.num_vertices];
119
120        for (idx, &selected) in config.iter().enumerate() {
121            if selected == 1 {
122                if let Some((u, v)) = self.edge_endpoints(idx) {
123                    if vertex_used[u] || vertex_used[v] {
124                        return false;
125                    }
126                    vertex_used[u] = true;
127                    vertex_used[v] = true;
128                }
129            }
130        }
131        true
132    }
133}
134
135impl<W> Problem for Matching<W>
136where
137    W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static,
138{
139    const NAME: &'static str = "Matching";
140    type GraphType = SimpleGraph;
141    type Weight = W;
142    type Size = W;
143
144    fn num_variables(&self) -> usize {
145        self.graph.edge_count() // Variables are edges
146    }
147
148    fn num_flavors(&self) -> usize {
149        2 // Binary: edge in matching or not
150    }
151
152    fn problem_size(&self) -> ProblemSize {
153        ProblemSize::new(vec![
154            ("num_vertices", self.num_vertices),
155            ("num_edges", self.graph.edge_count()),
156        ])
157    }
158
159    fn energy_mode(&self) -> EnergyMode {
160        EnergyMode::LargerSizeIsBetter // Maximize matching weight
161    }
162
163    fn solution_size(&self, config: &[usize]) -> SolutionSize<Self::Size> {
164        let is_valid = self.is_valid_matching(config);
165        let mut total = W::zero();
166        for (idx, &selected) in config.iter().enumerate() {
167            if selected == 1 {
168                if let Some(w) = self.edge_weights.get(idx) {
169                    total += w.clone();
170                }
171            }
172        }
173        SolutionSize::new(total, is_valid)
174    }
175}
176
177impl<W> ConstraintSatisfactionProblem for Matching<W>
178where
179    W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static,
180{
181    fn constraints(&self) -> Vec<LocalConstraint> {
182        let v2e = self.vertex_to_edges();
183        let mut constraints = Vec::new();
184
185        // For each vertex, at most one incident edge can be selected
186        for (_v, incident_edges) in v2e {
187            if incident_edges.len() < 2 {
188                continue; // No constraint needed for degree-0 or degree-1 vertices
189            }
190
191            let num_edges = incident_edges.len();
192            let num_configs = 2usize.pow(num_edges as u32);
193
194            // Valid if at most one edge is selected
195            let spec: Vec<bool> = (0..num_configs)
196                .map(|config_idx| {
197                    let count = (0..num_edges)
198                        .filter(|&i| (config_idx >> i) & 1 == 1)
199                        .count();
200                    count <= 1
201                })
202                .collect();
203
204            constraints.push(LocalConstraint::new(2, incident_edges, spec));
205        }
206
207        constraints
208    }
209
210    fn objectives(&self) -> Vec<LocalSolutionSize<Self::Size>> {
211        self.edge_weights
212            .iter()
213            .enumerate()
214            .map(|(i, w)| LocalSolutionSize::new(2, vec![i], vec![W::zero(), w.clone()]))
215            .collect()
216    }
217
218    fn weights(&self) -> Vec<Self::Size> {
219        self.edge_weights.clone()
220    }
221
222    fn set_weights(&mut self, weights: Vec<Self::Size>) {
223        assert_eq!(weights.len(), self.num_variables());
224        self.edge_weights = weights;
225    }
226
227    fn is_weighted(&self) -> bool {
228        if self.edge_weights.is_empty() {
229            return false;
230        }
231        let first = &self.edge_weights[0];
232        !self.edge_weights.iter().all(|w| w == first)
233    }
234}
235
236/// Check if a selection of edges forms a valid matching.
237pub fn is_matching(num_vertices: usize, edges: &[(usize, usize)], selected: &[bool]) -> bool {
238    if selected.len() != edges.len() {
239        return false;
240    }
241
242    let mut vertex_used = vec![false; num_vertices];
243    for (idx, &sel) in selected.iter().enumerate() {
244        if sel {
245            let (u, v) = edges[idx];
246            if u >= num_vertices || v >= num_vertices {
247                return false;
248            }
249            if vertex_used[u] || vertex_used[v] {
250                return false;
251            }
252            vertex_used[u] = true;
253            vertex_used[v] = true;
254        }
255    }
256    true
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::solvers::{BruteForce, Solver};
263
264    #[test]
265    fn test_matching_creation() {
266        let problem = Matching::new(4, vec![(0, 1, 1), (1, 2, 2), (2, 3, 3)]);
267        assert_eq!(problem.num_vertices(), 4);
268        assert_eq!(problem.num_edges(), 3);
269        assert_eq!(problem.num_variables(), 3);
270    }
271
272    #[test]
273    fn test_matching_unweighted() {
274        let problem = Matching::<i32>::unweighted(3, vec![(0, 1), (1, 2)]);
275        assert_eq!(problem.num_edges(), 2);
276    }
277
278    #[test]
279    fn test_edge_endpoints() {
280        let problem = Matching::new(3, vec![(0, 1, 1), (1, 2, 2)]);
281        assert_eq!(problem.edge_endpoints(0), Some((0, 1)));
282        assert_eq!(problem.edge_endpoints(1), Some((1, 2)));
283        assert_eq!(problem.edge_endpoints(2), None);
284    }
285
286    #[test]
287    fn test_is_valid_matching() {
288        let problem = Matching::new(4, vec![(0, 1, 1), (1, 2, 1), (2, 3, 1)]);
289
290        // Valid: select edge 0 only
291        assert!(problem.is_valid_matching(&[1, 0, 0]));
292
293        // Valid: select edges 0 and 2 (disjoint)
294        assert!(problem.is_valid_matching(&[1, 0, 1]));
295
296        // Invalid: edges 0 and 1 share vertex 1
297        assert!(!problem.is_valid_matching(&[1, 1, 0]));
298    }
299
300    #[test]
301    fn test_solution_size() {
302        let problem = Matching::new(4, vec![(0, 1, 5), (1, 2, 10), (2, 3, 3)]);
303
304        let sol = problem.solution_size(&[1, 0, 1]);
305        assert!(sol.is_valid);
306        assert_eq!(sol.size, 8); // 5 + 3
307
308        let sol = problem.solution_size(&[0, 1, 0]);
309        assert!(sol.is_valid);
310        assert_eq!(sol.size, 10);
311    }
312
313    #[test]
314    fn test_brute_force_path() {
315        // Path 0-1-2-3 with unit weights
316        let problem = Matching::<i32>::unweighted(4, vec![(0, 1), (1, 2), (2, 3)]);
317        let solver = BruteForce::new();
318
319        let solutions = solver.find_best(&problem);
320        // Maximum matching has 2 edges: {0-1, 2-3}
321        assert!(solutions.contains(&vec![1, 0, 1]));
322        for sol in &solutions {
323            assert_eq!(problem.solution_size(sol).size, 2);
324        }
325    }
326
327    #[test]
328    fn test_brute_force_triangle() {
329        let problem = Matching::<i32>::unweighted(3, vec![(0, 1), (1, 2), (0, 2)]);
330        let solver = BruteForce::new();
331
332        let solutions = solver.find_best(&problem);
333        // Maximum matching has 1 edge (any of the 3)
334        for sol in &solutions {
335            assert_eq!(sol.iter().sum::<usize>(), 1);
336            assert!(problem.solution_size(sol).is_valid);
337        }
338    }
339
340    #[test]
341    fn test_brute_force_weighted() {
342        // Prefer heavy edge even if it excludes more edges
343        let problem = Matching::new(4, vec![(0, 1, 100), (0, 2, 1), (1, 3, 1)]);
344        let solver = BruteForce::new();
345
346        let solutions = solver.find_best(&problem);
347        // Edge 0-1 (weight 100) alone beats edges 0-2 + 1-3 (weight 2)
348        assert!(solutions.contains(&vec![1, 0, 0]));
349    }
350
351    #[test]
352    fn test_is_matching_function() {
353        let edges = vec![(0, 1), (1, 2), (2, 3)];
354
355        assert!(is_matching(4, &edges, &[true, false, true])); // Disjoint
356        assert!(is_matching(4, &edges, &[false, true, false])); // Single edge
357        assert!(!is_matching(4, &edges, &[true, true, false])); // Share vertex 1
358        assert!(is_matching(4, &edges, &[false, false, false])); // Empty is valid
359    }
360
361    #[test]
362    fn test_energy_mode() {
363        let problem = Matching::<i32>::unweighted(2, vec![(0, 1)]);
364        assert!(problem.energy_mode().is_maximization());
365    }
366
367    #[test]
368    fn test_empty_graph() {
369        let problem = Matching::<i32>::unweighted(3, vec![]);
370        let sol = problem.solution_size(&[]);
371        assert!(sol.is_valid);
372        assert_eq!(sol.size, 0);
373    }
374
375    #[test]
376    fn test_constraints() {
377        let problem = Matching::<i32>::unweighted(3, vec![(0, 1), (1, 2)]);
378        let constraints = problem.constraints();
379        // Vertex 1 has degree 2, so 1 constraint
380        assert_eq!(constraints.len(), 1);
381    }
382
383    #[test]
384    fn test_edges() {
385        let problem = Matching::new(3, vec![(0, 1, 5), (1, 2, 10)]);
386        let edges = problem.edges();
387        assert_eq!(edges.len(), 2);
388    }
389
390    #[test]
391    fn test_perfect_matching() {
392        // K4: can have perfect matching (2 edges covering all 4 vertices)
393        let problem = Matching::<i32>::unweighted(
394            4,
395            vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
396        );
397        let solver = BruteForce::new();
398
399        let solutions = solver.find_best(&problem);
400        // Perfect matching has 2 edges
401        for sol in &solutions {
402            assert_eq!(problem.solution_size(sol).size, 2);
403            // Check it's a valid matching using 4 vertices
404            let mut used = [false; 4];
405            for (idx, &sel) in sol.iter().enumerate() {
406                if sel == 1 {
407                    if let Some((u, v)) = problem.edge_endpoints(idx) {
408                        used[u] = true;
409                        used[v] = true;
410                    }
411                }
412            }
413            assert!(used.iter().all(|&u| u)); // All vertices matched
414        }
415    }
416
417    #[test]
418    fn test_is_satisfied() {
419        let problem = Matching::<i32>::unweighted(4, vec![(0, 1), (1, 2), (2, 3)]);
420
421        assert!(problem.is_satisfied(&[1, 0, 1])); // Valid matching
422        assert!(problem.is_satisfied(&[0, 1, 0])); // Valid matching
423        assert!(!problem.is_satisfied(&[1, 1, 0])); // Share vertex 1
424    }
425
426    #[test]
427    fn test_objectives() {
428        let problem = Matching::new(3, vec![(0, 1, 5), (1, 2, 10)]);
429        let objectives = problem.objectives();
430        assert_eq!(objectives.len(), 2);
431    }
432
433    #[test]
434    fn test_set_weights() {
435        let mut problem = Matching::<i32>::unweighted(3, vec![(0, 1), (1, 2)]);
436        assert!(!problem.is_weighted()); // Initially uniform
437        problem.set_weights(vec![1, 2]);
438        assert!(problem.is_weighted());
439        assert_eq!(problem.weights(), vec![1, 2]);
440    }
441
442    #[test]
443    fn test_is_weighted_empty() {
444        let problem = Matching::<i32>::unweighted(2, vec![]);
445        assert!(!problem.is_weighted());
446    }
447
448    #[test]
449    fn test_is_matching_wrong_len() {
450        let edges = vec![(0, 1), (1, 2)];
451        assert!(!is_matching(3, &edges, &[true])); // Wrong length
452    }
453
454    #[test]
455    fn test_is_matching_out_of_bounds() {
456        let edges = vec![(0, 5)]; // Vertex 5 doesn't exist
457        assert!(!is_matching(3, &edges, &[true]));
458    }
459
460    #[test]
461    fn test_problem_size() {
462        let problem = Matching::<i32>::unweighted(5, vec![(0, 1), (1, 2), (2, 3)]);
463        let size = problem.problem_size();
464        assert_eq!(size.get("num_vertices"), Some(5));
465        assert_eq!(size.get("num_edges"), Some(3));
466    }
467}