Skip to main content

rust_igraph/algorithms/properties/
matching.rs

1//! Graph matching metrics (ALGO-TR-030).
2//!
3//! A **matching** is a set of edges with no shared endpoints.
4//!
5//! - **Greedy maximal matching**: iteratively pick edges greedily.
6//! - **Maximum matching** (brute-force): the largest matching.
7//!   NP-easy (polynomial via Edmonds' algorithm) but we use brute-force
8//!   for simplicity — suitable for small graphs (≤ ~25 vertices).
9//! - **Matching number**: `ν(G) = |maximum matching|`.
10//! - **Is perfect matching**: whether the matching covers all vertices.
11//! - **Edge independence number** (same as matching number).
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::too_many_lines
19)]
20
21use crate::core::{Graph, IgraphError, IgraphResult};
22
23/// Find a greedy maximal matching.
24///
25/// Iterates over edges and greedily adds each edge whose endpoints are
26/// both unmatched. Returns edge indices of the matching.
27///
28/// The result is maximal (no edge can be added) but not necessarily
29/// maximum.
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, greedy_matching};
35///
36/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
37/// let m = greedy_matching(&g).unwrap();
38/// assert!(!m.is_empty());
39/// ```
40pub fn greedy_matching(graph: &Graph) -> IgraphResult<Vec<usize>> {
41    let n = graph.vcount() as usize;
42    let mut matched = vec![false; n];
43    let mut matching = Vec::new();
44
45    for (eidx, (u, v)) in graph.edges().enumerate() {
46        let ui = u as usize;
47        let vi = v as usize;
48        if !matched[ui] && !matched[vi] {
49            matching.push(eidx);
50            matched[ui] = true;
51            matched[vi] = true;
52        }
53    }
54
55    Ok(matching)
56}
57
58/// Find the maximum matching (brute-force).
59///
60/// Returns edge indices of the largest matching. Only feasible for
61/// small graphs.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, maximum_matching};
67///
68/// // Path 0-1-2-3: max matching has 2 edges
69/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
70/// let m = maximum_matching(&g).unwrap();
71/// assert_eq!(m.len(), 2);
72/// ```
73pub fn maximum_matching(graph: &Graph) -> IgraphResult<Vec<usize>> {
74    let edges: Vec<(u32, u32)> = graph.edges().collect();
75    let m = edges.len();
76    let n = graph.vcount() as usize;
77
78    if m == 0 || n < 2 {
79        return Ok(Vec::new());
80    }
81
82    let mut best: Vec<usize> = Vec::new();
83    max_matching_branch(
84        &edges,
85        n,
86        0,
87        &mut Vec::new(),
88        &mut vec![false; n],
89        &mut best,
90    );
91
92    Ok(best)
93}
94
95fn max_matching_branch(
96    edges: &[(u32, u32)],
97    n: usize,
98    start: usize,
99    current: &mut Vec<usize>,
100    matched: &mut Vec<bool>,
101    best: &mut Vec<usize>,
102) {
103    if current.len() > best.len() {
104        *best = current.clone();
105    }
106
107    let remaining_possible = (edges.len() - start).min(n / 2);
108    if current.len() + remaining_possible <= best.len() {
109        return;
110    }
111
112    for i in start..edges.len() {
113        let (u, v) = edges[i];
114        let ui = u as usize;
115        let vi = v as usize;
116        if !matched[ui] && !matched[vi] {
117            matched[ui] = true;
118            matched[vi] = true;
119            current.push(i);
120            max_matching_branch(edges, n, i + 1, current, matched, best);
121            current.pop();
122            matched[ui] = false;
123            matched[vi] = false;
124        }
125    }
126}
127
128/// Compute the matching number `ν(G)`.
129///
130/// The size of the maximum matching. Also called the edge
131/// independence number.
132///
133/// # Examples
134///
135/// ```
136/// use rust_igraph::{Graph, matching_number};
137///
138/// // K_4: perfect matching exists, ν = 2
139/// let g = Graph::from_edges(
140///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
141/// ).unwrap();
142/// assert_eq!(matching_number(&g).unwrap(), 2);
143/// ```
144pub fn matching_number(graph: &Graph) -> IgraphResult<u32> {
145    let m = maximum_matching(graph)?;
146    Ok(m.len() as u32)
147}
148
149/// Check whether a matching is valid.
150///
151/// A valid matching has no two edges sharing an endpoint.
152///
153/// # Examples
154///
155/// ```
156/// use rust_igraph::{Graph, greedy_matching, is_valid_matching};
157///
158/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
159/// let m = greedy_matching(&g).unwrap();
160/// assert!(is_valid_matching(&g, &m).unwrap());
161/// ```
162pub fn is_valid_matching(graph: &Graph, edge_indices: &[usize]) -> IgraphResult<bool> {
163    let n = graph.vcount() as usize;
164    let edges: Vec<(u32, u32)> = graph.edges().collect();
165    let m = edges.len();
166
167    let mut used = vec![false; n];
168
169    for &eidx in edge_indices {
170        if eidx >= m {
171            return Err(IgraphError::InvalidArgument(format!(
172                "is_valid_matching: edge index {eidx} out of range (ecount={m})"
173            )));
174        }
175        let (u, v) = edges[eidx];
176        let ui = u as usize;
177        let vi = v as usize;
178        if used[ui] || used[vi] {
179            return Ok(false);
180        }
181        used[ui] = true;
182        used[vi] = true;
183    }
184
185    Ok(true)
186}
187
188/// Check whether a matching is a perfect matching.
189///
190/// A perfect matching covers every vertex exactly once. Only possible
191/// when the graph has an even number of vertices.
192///
193/// # Examples
194///
195/// ```
196/// use rust_igraph::{Graph, maximum_matching, is_perfect_matching};
197///
198/// // K_4 has a perfect matching
199/// let g = Graph::from_edges(
200///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
201/// ).unwrap();
202/// let m = maximum_matching(&g).unwrap();
203/// assert!(is_perfect_matching(&g, &m).unwrap());
204/// ```
205pub fn is_perfect_matching(graph: &Graph, edge_indices: &[usize]) -> IgraphResult<bool> {
206    let n = graph.vcount() as usize;
207    if n % 2 != 0 {
208        return Ok(false);
209    }
210    if edge_indices.len() != n / 2 {
211        return Ok(false);
212    }
213    is_valid_matching(graph, edge_indices)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn path4() -> Graph {
221        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
222    }
223
224    fn k3() -> Graph {
225        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
226    }
227
228    fn k4() -> Graph {
229        Graph::from_edges(
230            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
231            false,
232            Some(4),
233        )
234        .unwrap()
235    }
236
237    fn cycle4() -> Graph {
238        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
239    }
240
241    fn cycle5() -> Graph {
242        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
243    }
244
245    fn star5() -> Graph {
246        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
247    }
248
249    fn petersen() -> Graph {
250        Graph::from_edges(
251            &[
252                (0, 1),
253                (1, 2),
254                (2, 3),
255                (3, 4),
256                (4, 0),
257                (0, 5),
258                (1, 6),
259                (2, 7),
260                (3, 8),
261                (4, 9),
262                (5, 7),
263                (7, 9),
264                (9, 6),
265                (6, 8),
266                (8, 5),
267            ],
268            false,
269            Some(10),
270        )
271        .unwrap()
272    }
273
274    // --- greedy_matching ---
275
276    #[test]
277    fn gm_empty() {
278        let g = Graph::with_vertices(0);
279        assert!(greedy_matching(&g).unwrap().is_empty());
280    }
281
282    #[test]
283    fn gm_single() {
284        let g = Graph::with_vertices(1);
285        assert!(greedy_matching(&g).unwrap().is_empty());
286    }
287
288    #[test]
289    fn gm_path4() {
290        let g = path4();
291        let m = greedy_matching(&g).unwrap();
292        assert!(is_valid_matching(&g, &m).unwrap());
293        assert!(!m.is_empty());
294    }
295
296    #[test]
297    fn gm_k4() {
298        let g = k4();
299        let m = greedy_matching(&g).unwrap();
300        assert!(is_valid_matching(&g, &m).unwrap());
301    }
302
303    // --- maximum_matching ---
304
305    #[test]
306    fn mm_empty() {
307        let g = Graph::with_vertices(0);
308        assert!(maximum_matching(&g).unwrap().is_empty());
309    }
310
311    #[test]
312    fn mm_single_edge() {
313        let g = Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap();
314        let m = maximum_matching(&g).unwrap();
315        assert_eq!(m.len(), 1);
316    }
317
318    #[test]
319    fn mm_path4() {
320        let g = path4();
321        let m = maximum_matching(&g).unwrap();
322        assert_eq!(m.len(), 2);
323        assert!(is_valid_matching(&g, &m).unwrap());
324    }
325
326    #[test]
327    fn mm_k3() {
328        let g = k3();
329        let m = maximum_matching(&g).unwrap();
330        assert_eq!(m.len(), 1);
331    }
332
333    #[test]
334    fn mm_k4() {
335        let g = k4();
336        let m = maximum_matching(&g).unwrap();
337        assert_eq!(m.len(), 2);
338        assert!(is_perfect_matching(&g, &m).unwrap());
339    }
340
341    #[test]
342    fn mm_cycle4() {
343        let g = cycle4();
344        let m = maximum_matching(&g).unwrap();
345        assert_eq!(m.len(), 2);
346        assert!(is_perfect_matching(&g, &m).unwrap());
347    }
348
349    #[test]
350    fn mm_cycle5() {
351        let g = cycle5();
352        let m = maximum_matching(&g).unwrap();
353        assert_eq!(m.len(), 2);
354    }
355
356    #[test]
357    fn mm_star5() {
358        let g = star5();
359        let m = maximum_matching(&g).unwrap();
360        assert_eq!(m.len(), 1);
361    }
362
363    #[test]
364    fn mm_petersen() {
365        let g = petersen();
366        let m = maximum_matching(&g).unwrap();
367        assert_eq!(m.len(), 5);
368        assert!(is_perfect_matching(&g, &m).unwrap());
369    }
370
371    #[test]
372    fn mm_isolated() {
373        let g = Graph::with_vertices(4);
374        let m = maximum_matching(&g).unwrap();
375        assert!(m.is_empty());
376    }
377
378    // --- matching_number ---
379
380    #[test]
381    fn mn_k4() {
382        assert_eq!(matching_number(&k4()).unwrap(), 2);
383    }
384
385    #[test]
386    fn mn_path4() {
387        assert_eq!(matching_number(&path4()).unwrap(), 2);
388    }
389
390    #[test]
391    fn mn_star5() {
392        assert_eq!(matching_number(&star5()).unwrap(), 1);
393    }
394
395    // --- is_valid_matching ---
396
397    #[test]
398    fn ivm_valid() {
399        let g = path4();
400        assert!(is_valid_matching(&g, &[0, 2]).unwrap());
401    }
402
403    #[test]
404    fn ivm_invalid() {
405        let g = path4();
406        assert!(!is_valid_matching(&g, &[0, 1]).unwrap());
407    }
408
409    #[test]
410    fn ivm_empty() {
411        let g = path4();
412        assert!(is_valid_matching(&g, &[]).unwrap());
413    }
414
415    #[test]
416    fn ivm_out_of_range() {
417        let g = path4();
418        assert!(is_valid_matching(&g, &[10]).is_err());
419    }
420
421    // --- is_perfect_matching ---
422
423    #[test]
424    fn ipm_k4_perfect() {
425        let g = k4();
426        let m = maximum_matching(&g).unwrap();
427        assert!(is_perfect_matching(&g, &m).unwrap());
428    }
429
430    #[test]
431    fn ipm_odd_vertices() {
432        let g = k3();
433        let m = maximum_matching(&g).unwrap();
434        assert!(!is_perfect_matching(&g, &m).unwrap());
435    }
436
437    #[test]
438    fn ipm_not_enough_edges() {
439        let g = k4();
440        assert!(!is_perfect_matching(&g, &[0]).unwrap());
441    }
442
443    // --- cross-consistency ---
444
445    #[test]
446    fn greedy_is_valid() {
447        for g in &[path4(), k3(), k4(), cycle4(), cycle5(), star5(), petersen()] {
448            let m = greedy_matching(g).unwrap();
449            assert!(is_valid_matching(g, &m).unwrap());
450        }
451    }
452
453    #[test]
454    fn greedy_at_most_maximum() {
455        for g in &[path4(), k3(), k4(), cycle5(), star5()] {
456            let gm = greedy_matching(g).unwrap();
457            let mm = maximum_matching(g).unwrap();
458            assert!(gm.len() <= mm.len());
459        }
460    }
461
462    #[test]
463    fn matching_number_bounded() {
464        for g in &[path4(), k3(), k4(), cycle5(), star5()] {
465            let nu = matching_number(g).unwrap();
466            let n = g.vcount();
467            assert!(nu <= n / 2);
468        }
469    }
470
471    #[test]
472    fn gallai_identity() {
473        // Gallai's theorem: α + ν = n (for graphs without isolated vertices
474        // where α is vertex cover number = n - independence number)
475        // Actually: matching_number + vertex_cover_number = n
476        // And: independence_number + vertex_cover_number = n
477        // So: independence_number + matching_number = n only for König graphs
478        // (bipartite). Let's test König's theorem for bipartite graphs.
479        let g = path4(); // bipartite
480        let alpha = crate::algorithms::cliques::independence_number(&g).unwrap();
481        let nu = matching_number(&g).unwrap();
482        let n = g.vcount();
483        assert_eq!(alpha + nu, n);
484    }
485}