Skip to main content

oxicuda_graphalg/matching/
weighted_general.rs

1//! Maximum-weight matching on a general (non-bipartite) weighted graph.
2//!
3//! Implements Edmonds' weighted blossom algorithm in the `O(n^3)` primal-dual
4//! array formulation of Galil (1986) — the same formulation underlying Van
5//! Rantwijk's `mwmatching` / NetworkX `max_weight_matching`. Unlike the
6//! cardinality-only [`blossom_v_simple`](super::blossom_v_simple), this finds a
7//! matching of **maximum total weight** (which need not be a maximum-cardinality
8//! matching), maintaining vertex potentials `y_v` and blossom potentials `z_B`
9//! with the usual complementary-slackness invariants:
10//!
11//! * `slack(u,v) = y_u + y_v - 2 w(u,v) >= 0` for every edge,
12//! * matched edges and full blossoms are tight (slack `0`),
13//! * unmatched vertices keep `y_v >= 0` (so leaving a vertex unmatched is optimal
14//!   exactly when `y_v = 0`).
15//!
16//! Alternating trees are grown from free vertices; odd cycles are contracted
17//! into blossoms and expanded as the dual variables are adjusted. The public
18//! entry point is [`WeightedGeneralMatching`].
19
20use std::collections::HashMap;
21
22use crate::error::{GraphalgError, GraphalgResult};
23use crate::repr::weighted_graph::WeightedGraph;
24
25/// Result of a maximum-weight general matching solve.
26#[derive(Debug, Clone, PartialEq)]
27pub struct WeightedMatchingResult {
28    /// `mate[v] = Some(u)` iff `v` is matched to `u`; `None` if unmatched.
29    pub mate: Vec<Option<usize>>,
30    /// The matched edges as `(u, v, weight)` with `u < v`, sorted.
31    pub matched_edges: Vec<(usize, usize, f64)>,
32    /// Total weight of the matching.
33    pub total_weight: f64,
34}
35
36/// Maximum-weight matching solver for a general undirected weighted graph.
37#[derive(Debug, Clone)]
38pub struct WeightedGeneralMatching {
39    n: usize,
40    /// Canonical `(min, max) -> weight` map (parallel edges keep the max weight,
41    /// self-loops ignored).
42    pair_weight: HashMap<(usize, usize), f64>,
43}
44
45impl WeightedGeneralMatching {
46    /// Create an edgeless instance on `n` vertices.
47    pub fn new(n: usize) -> Self {
48        Self {
49            n,
50            pair_weight: HashMap::new(),
51        }
52    }
53
54    /// Add an undirected weighted edge `(u, v, w)`.
55    ///
56    /// Self-loops are ignored. Parallel edges are de-duplicated keeping the
57    /// largest weight (only positive-contribution edges ever enter a maximum
58    /// weight matching).
59    pub fn add_edge(&mut self, u: usize, v: usize, w: f64) -> GraphalgResult<()> {
60        if u >= self.n || v >= self.n {
61            return Err(GraphalgError::IndexOutOfBounds {
62                index: u.max(v),
63                len: self.n,
64            });
65        }
66        if !w.is_finite() {
67            return Err(GraphalgError::InvalidEdgeWeight(format!(
68                "edge ({u},{v}) weight not finite: {w}"
69            )));
70        }
71        if u == v {
72            return Ok(());
73        }
74        let key = if u < v { (u, v) } else { (v, u) };
75        self.pair_weight
76            .entry(key)
77            .and_modify(|old| {
78                if w > *old {
79                    *old = w;
80                }
81            })
82            .or_insert(w);
83        Ok(())
84    }
85
86    /// Build a solver from a [`WeightedGraph`] (treated as undirected; the larger
87    /// weight is kept for each pair).
88    pub fn from_weighted_graph(g: &WeightedGraph) -> GraphalgResult<Self> {
89        let mut m = Self::new(g.n);
90        for (u, v, w) in g.all_edges() {
91            m.add_edge(u, v, w)?;
92        }
93        Ok(m)
94    }
95
96    /// Number of vertices.
97    pub fn num_nodes(&self) -> usize {
98        self.n
99    }
100
101    /// The de-duplicated edge list as `(u, v, w)` with `u < v`, sorted.
102    pub fn edges(&self) -> Vec<(usize, usize, f64)> {
103        let mut e: Vec<(usize, usize, f64)> = self
104            .pair_weight
105            .iter()
106            .map(|(&(u, v), &w)| (u, v, w))
107            .collect();
108        e.sort_by_key(|a| (a.0, a.1));
109        e
110    }
111
112    /// Solve for a **maximum-weight** matching (not necessarily of maximum
113    /// cardinality).
114    pub fn solve(&self) -> GraphalgResult<WeightedMatchingResult> {
115        self.solve_inner(false)
116    }
117
118    /// Solve for a maximum-weight matching **among the maximum-cardinality**
119    /// matchings.
120    pub fn solve_max_cardinality(&self) -> GraphalgResult<WeightedMatchingResult> {
121        self.solve_inner(true)
122    }
123
124    fn solve_inner(&self, maxcardinality: bool) -> GraphalgResult<WeightedMatchingResult> {
125        let edges = self.edges();
126        let mate_vertices = if edges.is_empty() || self.n == 0 {
127            vec![-1i64; self.n]
128        } else {
129            let mut matcher = Matcher::new(self.n, &edges, maxcardinality);
130            matcher.run()?;
131            matcher.mate
132        };
133        let mut mate = vec![None; self.n];
134        let mut matched_edges = Vec::new();
135        let mut total_weight = 0.0;
136        for v in 0..self.n {
137            if mate_vertices[v] >= 0 {
138                let u = mate_vertices[v] as usize;
139                mate[v] = Some(u);
140                if v < u {
141                    let key = (v, u);
142                    let w = *self.pair_weight.get(&key).unwrap_or(&0.0);
143                    matched_edges.push((v, u, w));
144                    total_weight += w;
145                }
146            }
147        }
148        matched_edges.sort_by_key(|a| (a.0, a.1));
149        Ok(WeightedMatchingResult {
150            mate,
151            matched_edges,
152            total_weight,
153        })
154    }
155}
156
157/// Python-style modular index into a length-`len` ring (`len > 0`).
158fn ring_index(idx: isize, len: isize) -> usize {
159    (((idx % len) + len) % len) as usize
160}
161
162/// Internal blossom matcher (array formulation). Index conventions follow Galil
163/// / Van Rantwijk: an *endpoint* `p` refers to `edges[p / 2]`, with `p ^ 1`
164/// flipping between the two endpoints of an edge; `endpoint[p]` is the vertex at
165/// endpoint `p`.
166struct Matcher {
167    nvertex: usize,
168    edges: Vec<(usize, usize, f64)>,
169    endpoint: Vec<usize>,
170    neighbend: Vec<Vec<usize>>,
171    /// `mate[v]` = matched endpoint (then converted to vertex), or `-1`.
172    mate: Vec<i64>,
173    /// Label: `-1` removed, `0` free, `1` S/outer, `2` T/inner, `5` scan-temp.
174    label: Vec<i64>,
175    labelend: Vec<i64>,
176    inblossom: Vec<usize>,
177    blossomparent: Vec<i64>,
178    blossomchilds: Vec<Vec<usize>>,
179    blossombase: Vec<i64>,
180    blossomendps: Vec<Vec<usize>>,
181    bestedge: Vec<i64>,
182    blossombestedges: Vec<Option<Vec<usize>>>,
183    unusedblossoms: Vec<usize>,
184    dualvar: Vec<f64>,
185    allowedge: Vec<bool>,
186    queue: Vec<usize>,
187    maxcardinality: bool,
188}
189
190impl Matcher {
191    fn new(nvertex: usize, edges: &[(usize, usize, f64)], maxcardinality: bool) -> Self {
192        let nedge = edges.len();
193        let mut endpoint = vec![0usize; 2 * nedge];
194        for (k, &(i, j, _)) in edges.iter().enumerate() {
195            endpoint[2 * k] = i;
196            endpoint[2 * k + 1] = j;
197        }
198        let mut neighbend = vec![Vec::new(); nvertex];
199        for (k, &(i, j, _)) in edges.iter().enumerate() {
200            neighbend[i].push(2 * k + 1);
201            neighbend[j].push(2 * k);
202        }
203        let mut maxweight = 0.0f64;
204        for &(_, _, w) in edges {
205            if w > maxweight {
206                maxweight = w;
207            }
208        }
209        let mut dualvar = vec![0.0f64; 2 * nvertex];
210        for d in dualvar.iter_mut().take(nvertex) {
211            *d = maxweight;
212        }
213        Self {
214            nvertex,
215            edges: edges.to_vec(),
216            endpoint,
217            neighbend,
218            mate: vec![-1; nvertex],
219            label: vec![0; 2 * nvertex],
220            labelend: vec![-1; 2 * nvertex],
221            inblossom: (0..nvertex).collect(),
222            blossomparent: vec![-1; 2 * nvertex],
223            blossomchilds: vec![Vec::new(); 2 * nvertex],
224            blossombase: (0..nvertex as i64)
225                .chain(std::iter::repeat_n(-1, nvertex))
226                .collect(),
227            blossomendps: vec![Vec::new(); 2 * nvertex],
228            bestedge: vec![-1; 2 * nvertex],
229            blossombestedges: vec![None; 2 * nvertex],
230            unusedblossoms: (nvertex..2 * nvertex).collect(),
231            dualvar,
232            allowedge: vec![false; nedge],
233            queue: Vec::new(),
234            maxcardinality,
235        }
236    }
237
238    fn slack(&self, k: usize) -> f64 {
239        let (i, j, w) = self.edges[k];
240        self.dualvar[i] + self.dualvar[j] - 2.0 * w
241    }
242
243    fn blossom_leaves(&self, b: usize) -> Vec<usize> {
244        if b < self.nvertex {
245            return vec![b];
246        }
247        let mut out = Vec::new();
248        for &t in &self.blossomchilds[b] {
249            if t < self.nvertex {
250                out.push(t);
251            } else {
252                out.extend(self.blossom_leaves(t));
253            }
254        }
255        out
256    }
257
258    fn assign_label(&mut self, w: usize, t: i64, p: i64) {
259        let b = self.inblossom[w];
260        self.label[w] = t;
261        self.label[b] = t;
262        self.labelend[w] = p;
263        self.labelend[b] = p;
264        self.bestedge[w] = -1;
265        self.bestedge[b] = -1;
266        if t == 1 {
267            let leaves = self.blossom_leaves(b);
268            self.queue.extend(leaves);
269        } else if t == 2 {
270            let base = self.blossombase[b] as usize;
271            let mb = self.mate[base];
272            let endp = self.endpoint[mb as usize];
273            self.assign_label(endp, 1, mb ^ 1);
274        }
275    }
276
277    fn scan_blossom(&mut self, v: usize, w: usize) -> i64 {
278        let mut path: Vec<usize> = Vec::new();
279        let mut base: i64 = -1;
280        let mut v: i64 = v as i64;
281        let mut w: i64 = w as i64;
282        while v != -1 || w != -1 {
283            let b = self.inblossom[v as usize];
284            if self.label[b] & 4 != 0 {
285                base = self.blossombase[b];
286                break;
287            }
288            path.push(b);
289            self.label[b] = 5;
290            if self.labelend[b] == -1 {
291                v = -1;
292            } else {
293                v = self.endpoint[self.labelend[b] as usize] as i64;
294                let b2 = self.inblossom[v as usize];
295                v = self.endpoint[self.labelend[b2] as usize] as i64;
296            }
297            if w != -1 {
298                std::mem::swap(&mut v, &mut w);
299            }
300        }
301        for b in path {
302            self.label[b] = 1;
303        }
304        base
305    }
306
307    fn add_blossom(&mut self, base: usize, k: usize) -> GraphalgResult<()> {
308        let (v0, w0, _) = self.edges[k];
309        let bb = self.inblossom[base];
310        let b = self
311            .unusedblossoms
312            .pop()
313            .ok_or_else(|| GraphalgError::NumericalInstability("blossom pool exhausted".into()))?;
314        self.blossombase[b] = base as i64;
315        self.blossomparent[b] = -1;
316        self.blossomparent[bb] = b as i64;
317        let mut path: Vec<usize> = Vec::new();
318        let mut endps: Vec<usize> = Vec::new();
319        let mut v = v0;
320        let mut bv = self.inblossom[v0];
321        while bv != bb {
322            self.blossomparent[bv] = b as i64;
323            path.push(bv);
324            endps.push(self.labelend[bv] as usize);
325            v = self.endpoint[self.labelend[bv] as usize];
326            bv = self.inblossom[v];
327        }
328        path.push(bb);
329        path.reverse();
330        endps.reverse();
331        endps.push(2 * k);
332        let mut w = w0;
333        let mut bw = self.inblossom[w0];
334        while bw != bb {
335            self.blossomparent[bw] = b as i64;
336            path.push(bw);
337            endps.push((self.labelend[bw] as usize) ^ 1);
338            w = self.endpoint[self.labelend[bw] as usize];
339            bw = self.inblossom[w];
340        }
341        let _ = (v, w);
342        self.blossomchilds[b] = path;
343        self.blossomendps[b] = endps;
344        self.label[b] = 1;
345        self.labelend[b] = self.labelend[bb];
346        self.dualvar[b] = 0.0;
347        for leaf in self.blossom_leaves(b) {
348            if self.label[self.inblossom[leaf]] == 2 {
349                self.queue.push(leaf);
350            }
351            self.inblossom[leaf] = b;
352        }
353        // Compute best edges from the new blossom to each external S-structure.
354        let mut bestedgeto = vec![-1i64; 2 * self.nvertex];
355        let childs = self.blossomchilds[b].clone();
356        for &bv in &childs {
357            let nblists: Vec<Vec<usize>> = match &self.blossombestedges[bv] {
358                Some(list) => vec![list.clone()],
359                None => self
360                    .blossom_leaves(bv)
361                    .into_iter()
362                    .map(|leaf| {
363                        self.neighbend[leaf]
364                            .iter()
365                            .map(|&p| p / 2)
366                            .collect::<Vec<usize>>()
367                    })
368                    .collect(),
369            };
370            for nblist in nblists {
371                for kk in nblist {
372                    let (mut i, mut j, _) = self.edges[kk];
373                    if self.inblossom[j] == b {
374                        std::mem::swap(&mut i, &mut j);
375                    }
376                    let bj = self.inblossom[j];
377                    if bj != b
378                        && self.label[bj] == 1
379                        && (bestedgeto[bj] == -1
380                            || self.slack(kk) < self.slack(bestedgeto[bj] as usize))
381                    {
382                        bestedgeto[bj] = kk as i64;
383                    }
384                }
385            }
386            self.blossombestedges[bv] = None;
387            self.bestedge[bv] = -1;
388        }
389        let collected: Vec<usize> = bestedgeto
390            .iter()
391            .filter(|&&kk| kk != -1)
392            .map(|&kk| kk as usize)
393            .collect();
394        self.blossombestedges[b] = Some(collected.clone());
395        self.bestedge[b] = -1;
396        for kk in collected {
397            if self.bestedge[b] == -1 || self.slack(kk) < self.slack(self.bestedge[b] as usize) {
398                self.bestedge[b] = kk as i64;
399            }
400        }
401        Ok(())
402    }
403
404    fn expand_blossom(&mut self, b: usize, endstage: bool) -> GraphalgResult<()> {
405        let childs = self.blossomchilds[b].clone();
406        for &s in &childs {
407            self.blossomparent[s] = -1;
408            if s < self.nvertex {
409                self.inblossom[s] = s;
410            } else if endstage && self.dualvar[s] == 0.0 {
411                self.expand_blossom(s, endstage)?;
412            } else {
413                for leaf in self.blossom_leaves(s) {
414                    self.inblossom[leaf] = s;
415                }
416            }
417        }
418        if !endstage && self.label[b] == 2 {
419            let entry_ep = (self.labelend[b] as usize) ^ 1;
420            let entrychild = self.inblossom[self.endpoint[entry_ep]];
421            let childs_b = self.blossomchilds[b].clone();
422            let endps_b = self.blossomendps[b].clone();
423            let len = childs_b.len() as isize;
424            let pos = childs_b
425                .iter()
426                .position(|&c| c == entrychild)
427                .ok_or_else(|| GraphalgError::NumericalInstability("entrychild missing".into()))?;
428            let mut j = pos as isize;
429            let jstep: isize;
430            let endptrick: isize;
431            if j & 1 != 0 {
432                j -= len;
433                jstep = 1;
434                endptrick = 0;
435            } else {
436                jstep = -1;
437                endptrick = 1;
438            }
439            let mut p = self.labelend[b];
440            while j != 0 {
441                self.label[self.endpoint[(p as usize) ^ 1]] = 0;
442                let e1 = endps_b[ring_index(j - endptrick, len)];
443                self.label[self.endpoint[e1 ^ (endptrick as usize) ^ 1]] = 0;
444                let endp = self.endpoint[(p as usize) ^ 1];
445                self.assign_label(endp, 2, p);
446                self.allowedge[e1 / 2] = true;
447                j += jstep;
448                let e2 = endps_b[ring_index(j - endptrick, len)];
449                p = (e2 ^ (endptrick as usize)) as i64;
450                self.allowedge[(p as usize) / 2] = true;
451                j += jstep;
452            }
453            let bv = childs_b[ring_index(j, len)];
454            let endp = self.endpoint[(p as usize) ^ 1];
455            self.label[endp] = 2;
456            self.label[bv] = 2;
457            self.labelend[endp] = p;
458            self.labelend[bv] = p;
459            self.bestedge[bv] = -1;
460            j += jstep;
461            while childs_b[ring_index(j, len)] != entrychild {
462                let bv = childs_b[ring_index(j, len)];
463                if self.label[bv] == 1 {
464                    j += jstep;
465                    continue;
466                }
467                let mut vv = usize::MAX;
468                for leaf in self.blossom_leaves(bv) {
469                    if self.label[leaf] != 0 {
470                        vv = leaf;
471                        break;
472                    }
473                }
474                if vv != usize::MAX {
475                    self.label[vv] = 0;
476                    let base_bv = self.blossombase[bv] as usize;
477                    let mep = self.mate[base_bv];
478                    self.label[self.endpoint[mep as usize]] = 0;
479                    let le = self.labelend[vv];
480                    self.assign_label(vv, 2, le);
481                }
482                j += jstep;
483            }
484        }
485        self.label[b] = -1;
486        self.labelend[b] = -1;
487        self.blossomchilds[b] = Vec::new();
488        self.blossomendps[b] = Vec::new();
489        self.blossombase[b] = -1;
490        self.blossombestedges[b] = None;
491        self.bestedge[b] = -1;
492        self.unusedblossoms.push(b);
493        Ok(())
494    }
495
496    fn augment_blossom(&mut self, b: usize, v: usize) -> GraphalgResult<()> {
497        let mut t = v;
498        while self.blossomparent[t] != b as i64 {
499            t = self.blossomparent[t] as usize;
500        }
501        if t >= self.nvertex {
502            self.augment_blossom(t, v)?;
503        }
504        let childs_b = self.blossomchilds[b].clone();
505        let endps_b = self.blossomendps[b].clone();
506        let len = childs_b.len() as isize;
507        let i =
508            childs_b.iter().position(|&c| c == t).ok_or_else(|| {
509                GraphalgError::NumericalInstability("augment child missing".into())
510            })? as isize;
511        let mut j = i;
512        let jstep: isize;
513        let endptrick: isize;
514        if i & 1 != 0 {
515            j -= len;
516            jstep = 1;
517            endptrick = 0;
518        } else {
519            jstep = -1;
520            endptrick = 1;
521        }
522        while j != 0 {
523            j += jstep;
524            let t1 = childs_b[ring_index(j, len)];
525            let p = endps_b[ring_index(j - endptrick, len)] ^ (endptrick as usize);
526            if t1 >= self.nvertex {
527                let ep = self.endpoint[p];
528                self.augment_blossom(t1, ep)?;
529            }
530            j += jstep;
531            let t2 = childs_b[ring_index(j, len)];
532            if t2 >= self.nvertex {
533                let ep = self.endpoint[p ^ 1];
534                self.augment_blossom(t2, ep)?;
535            }
536            self.mate[self.endpoint[p]] = (p ^ 1) as i64;
537            self.mate[self.endpoint[p ^ 1]] = p as i64;
538        }
539        let iu = i as usize;
540        let new_childs: Vec<usize> = childs_b[iu..]
541            .iter()
542            .chain(childs_b[..iu].iter())
543            .copied()
544            .collect();
545        let new_endps: Vec<usize> = endps_b[iu..]
546            .iter()
547            .chain(endps_b[..iu].iter())
548            .copied()
549            .collect();
550        self.blossomchilds[b] = new_childs;
551        self.blossomendps[b] = new_endps;
552        self.blossombase[b] = self.blossombase[self.blossomchilds[b][0]];
553        Ok(())
554    }
555
556    fn augment_matching(&mut self, k: usize) -> GraphalgResult<()> {
557        let (v, w, _) = self.edges[k];
558        let starts = [(v, (2 * k + 1) as i64), (w, (2 * k) as i64)];
559        for &(s0, p0) in &starts {
560            let mut s = s0;
561            let mut p = p0;
562            loop {
563                let bs = self.inblossom[s];
564                if bs >= self.nvertex {
565                    self.augment_blossom(bs, s)?;
566                }
567                self.mate[s] = p;
568                if self.labelend[bs] == -1 {
569                    break;
570                }
571                let t = self.endpoint[self.labelend[bs] as usize];
572                let bt = self.inblossom[t];
573                s = self.endpoint[self.labelend[bt] as usize];
574                let j = self.endpoint[(self.labelend[bt] as usize) ^ 1];
575                if bt >= self.nvertex {
576                    self.augment_blossom(bt, j)?;
577                }
578                self.mate[j] = self.labelend[bt];
579                p = self.labelend[bt] ^ 1;
580            }
581        }
582        Ok(())
583    }
584
585    fn min_vertex_dual(&self) -> f64 {
586        let mut m = f64::INFINITY;
587        for v in 0..self.nvertex {
588            if self.dualvar[v] < m {
589                m = self.dualvar[v];
590            }
591        }
592        m
593    }
594
595    fn run(&mut self) -> GraphalgResult<()> {
596        let guard_cap = 16 * self.nvertex * self.nvertex + 1000;
597        for _phase in 0..self.nvertex {
598            for x in self.label.iter_mut() {
599                *x = 0;
600            }
601            for x in self.bestedge.iter_mut() {
602                *x = -1;
603            }
604            for b in self.nvertex..2 * self.nvertex {
605                self.blossombestedges[b] = None;
606            }
607            self.queue.clear();
608            for x in self.allowedge.iter_mut() {
609                *x = false;
610            }
611            for v in 0..self.nvertex {
612                if self.mate[v] == -1 && self.label[self.inblossom[v]] == 0 {
613                    self.assign_label(v, 1, -1);
614                }
615            }
616            let mut augmented = false;
617            let mut guard = 0usize;
618            loop {
619                guard += 1;
620                if guard > guard_cap {
621                    return Err(GraphalgError::NumericalInstability(
622                        "weighted matching did not converge".into(),
623                    ));
624                }
625                while !augmented {
626                    let v = match self.queue.pop() {
627                        Some(v) => v,
628                        None => break,
629                    };
630                    let neigh = self.neighbend[v].clone();
631                    for p in neigh {
632                        let k = p / 2;
633                        let w = self.endpoint[p];
634                        if self.inblossom[v] == self.inblossom[w] {
635                            continue;
636                        }
637                        if !self.allowedge[k] {
638                            let kslack = self.slack(k);
639                            if kslack <= 0.0 {
640                                self.allowedge[k] = true;
641                            }
642                        }
643                        if self.allowedge[k] {
644                            if self.label[self.inblossom[w]] == 0 {
645                                self.assign_label(w, 2, (p ^ 1) as i64);
646                            } else if self.label[self.inblossom[w]] == 1 {
647                                let base = self.scan_blossom(v, w);
648                                if base >= 0 {
649                                    self.add_blossom(base as usize, k)?;
650                                } else {
651                                    self.augment_matching(k)?;
652                                    augmented = true;
653                                    break;
654                                }
655                            } else if self.label[w] == 0 {
656                                self.label[w] = 2;
657                                self.labelend[w] = (p ^ 1) as i64;
658                            }
659                        } else if self.label[self.inblossom[w]] == 1 {
660                            let bvv = self.inblossom[v];
661                            if self.bestedge[bvv] == -1
662                                || self.slack(k) < self.slack(self.bestedge[bvv] as usize)
663                            {
664                                self.bestedge[bvv] = k as i64;
665                            }
666                        } else if self.label[w] == 0
667                            && (self.bestedge[w] == -1
668                                || self.slack(k) < self.slack(self.bestedge[w] as usize))
669                        {
670                            self.bestedge[w] = k as i64;
671                        }
672                    }
673                }
674                if augmented {
675                    break;
676                }
677                let mut deltatype: i32 = -1;
678                let mut delta = 0.0f64;
679                let mut deltaedge: i64 = -1;
680                let mut deltablossom: i64 = -1;
681                if !self.maxcardinality {
682                    deltatype = 1;
683                    delta = self.min_vertex_dual().max(0.0);
684                }
685                for v in 0..self.nvertex {
686                    if self.label[self.inblossom[v]] == 0 && self.bestedge[v] != -1 {
687                        let d = self.slack(self.bestedge[v] as usize);
688                        if deltatype == -1 || d < delta {
689                            delta = d;
690                            deltatype = 2;
691                            deltaedge = self.bestedge[v];
692                        }
693                    }
694                }
695                for b in 0..2 * self.nvertex {
696                    if self.blossomparent[b] == -1 && self.label[b] == 1 && self.bestedge[b] != -1 {
697                        let d = self.slack(self.bestedge[b] as usize) / 2.0;
698                        if deltatype == -1 || d < delta {
699                            delta = d;
700                            deltatype = 3;
701                            deltaedge = self.bestedge[b];
702                        }
703                    }
704                }
705                for b in self.nvertex..2 * self.nvertex {
706                    if self.blossombase[b] >= 0
707                        && self.blossomparent[b] == -1
708                        && self.label[b] == 2
709                        && (deltatype == -1 || self.dualvar[b] < delta)
710                    {
711                        delta = self.dualvar[b];
712                        deltatype = 4;
713                        deltablossom = b as i64;
714                    }
715                }
716                if deltatype == -1 {
717                    deltatype = 1;
718                    delta = self.min_vertex_dual().max(0.0);
719                }
720                for v in 0..self.nvertex {
721                    let lbl = self.label[self.inblossom[v]];
722                    if lbl == 1 {
723                        self.dualvar[v] -= delta;
724                    } else if lbl == 2 {
725                        self.dualvar[v] += delta;
726                    }
727                }
728                for b in self.nvertex..2 * self.nvertex {
729                    if self.blossombase[b] >= 0 && self.blossomparent[b] == -1 {
730                        if self.label[b] == 1 {
731                            self.dualvar[b] += delta;
732                        } else if self.label[b] == 2 {
733                            self.dualvar[b] -= delta;
734                        }
735                    }
736                }
737                if deltatype == 1 {
738                    break;
739                } else if deltatype == 2 {
740                    let de = deltaedge as usize;
741                    self.allowedge[de] = true;
742                    let (mut i, j, _) = self.edges[de];
743                    if self.label[self.inblossom[i]] == 0 {
744                        i = j;
745                    }
746                    self.queue.push(i);
747                } else if deltatype == 3 {
748                    let de = deltaedge as usize;
749                    self.allowedge[de] = true;
750                    let (i, _, _) = self.edges[de];
751                    self.queue.push(i);
752                } else if deltatype == 4 {
753                    self.expand_blossom(deltablossom as usize, false)?;
754                }
755            }
756            if !augmented {
757                break;
758            }
759            for b in self.nvertex..2 * self.nvertex {
760                if self.blossomparent[b] == -1
761                    && self.blossombase[b] >= 0
762                    && self.label[b] == 1
763                    && self.dualvar[b] == 0.0
764                {
765                    self.expand_blossom(b, true)?;
766                }
767            }
768        }
769        for v in 0..self.nvertex {
770            if self.mate[v] >= 0 {
771                self.mate[v] = self.endpoint[self.mate[v] as usize] as i64;
772            }
773        }
774        Ok(())
775    }
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781    use crate::handle::LcgRng;
782
783    fn approx(a: f64, b: f64) -> bool {
784        (a - b).abs() < 1e-7
785    }
786
787    /// Validate the structural invariants of a matching result.
788    fn validate(res: &WeightedMatchingResult, edges: &[(usize, usize, f64)], n: usize) {
789        // mate is symmetric, no shared vertices.
790        let mut used = vec![false; n];
791        for v in 0..n {
792            if let Some(u) = res.mate[v] {
793                assert_eq!(res.mate[u], Some(v), "mate not symmetric at {v}");
794                assert!(!used[v], "vertex {v} shared by two matched edges");
795                used[v] = true;
796            }
797        }
798        // Reported total weight equals the summed weights of matched edges.
799        let mut sum = 0.0;
800        for &(u, v, w) in &res.matched_edges {
801            assert_eq!(res.mate[u], Some(v));
802            // weight matches an actual edge
803            let found = edges
804                .iter()
805                .any(|&(a, b, ww)| ((a, b) == (u, v) || (a, b) == (v, u)) && approx(ww, w));
806            assert!(found, "matched edge ({u},{v},{w}) not in graph");
807            sum += w;
808        }
809        assert!(approx(sum, res.total_weight), "weight mismatch");
810    }
811
812    fn edge_weight(edges: &[(usize, usize, f64)], i: usize, j: usize) -> Option<f64> {
813        let mut best: Option<f64> = None;
814        for &(a, b, w) in edges {
815            if (a, b) == (i, j) || (a, b) == (j, i) {
816                best = Some(best.map_or(w, |x: f64| x.max(w)));
817            }
818        }
819        best
820    }
821
822    /// Exact maximum-weight matching via subset DP (oracle), `n <= ~12`.
823    fn brute_max_weight(n: usize, edges: &[(usize, usize, f64)]) -> f64 {
824        let full = 1usize << n;
825        let mut dp = vec![0.0f64; full];
826        for mask in 1..full {
827            let i = mask.trailing_zeros() as usize;
828            let without_i = mask & !(1 << i);
829            let mut best = dp[without_i];
830            let mut rest = without_i;
831            while rest != 0 {
832                let jb = rest.trailing_zeros() as usize;
833                if let Some(w) = edge_weight(edges, i, jb) {
834                    let cand = w + dp[without_i & !(1 << jb)];
835                    if cand > best {
836                        best = cand;
837                    }
838                }
839                rest &= rest - 1;
840            }
841            dp[mask] = best;
842        }
843        dp[full - 1]
844    }
845
846    #[test]
847    fn triangle_picks_heaviest_edge() {
848        let mut m = WeightedGeneralMatching::new(3);
849        m.add_edge(0, 1, 3.0).expect("e");
850        m.add_edge(1, 2, 5.0).expect("e");
851        m.add_edge(0, 2, 4.0).expect("e");
852        let res = m.solve().expect("solve");
853        validate(&res, &m.edges(), 3);
854        assert!(approx(res.total_weight, 5.0), "got {}", res.total_weight);
855        assert_eq!(res.matched_edges, vec![(1, 2, 5.0)]);
856    }
857
858    #[test]
859    fn empty_and_single_vertex() {
860        let m0 = WeightedGeneralMatching::new(0);
861        let r0 = m0.solve().expect("solve");
862        assert!(r0.matched_edges.is_empty());
863        assert!(approx(r0.total_weight, 0.0));
864
865        let m1 = WeightedGeneralMatching::new(1);
866        let r1 = m1.solve().expect("solve");
867        assert_eq!(r1.mate, vec![None]);
868        assert!(approx(r1.total_weight, 0.0));
869    }
870
871    #[test]
872    fn path_optimal_alternating_set() {
873        // Path 0-1-2-3-4 with weights 1,2,2,1.
874        // Optimal: edges (1,2) and ... no, (1,2) blocks 0 and 3. Best alternating:
875        // (0,1)+(2,3)=1+2=3, (1,2)+(3,4)=2+1=3, (0,1)+(3,4)=1+1=2 ... max = 3 via
876        // (1,2)+(3,4) or (0,1)+(2,3). Verified against brute force below.
877        let mut m = WeightedGeneralMatching::new(5);
878        let edges = [(0, 1, 1.0), (1, 2, 2.0), (2, 3, 2.0), (3, 4, 1.0)];
879        for &(u, v, w) in &edges {
880            m.add_edge(u, v, w).expect("e");
881        }
882        let res = m.solve().expect("solve");
883        validate(&res, &m.edges(), 5);
884        let brute = brute_max_weight(5, &m.edges());
885        assert!(
886            approx(res.total_weight, brute),
887            "got {} brute {brute}",
888            res.total_weight
889        );
890        assert!(approx(res.total_weight, 3.0));
891    }
892
893    #[test]
894    fn blossom_needed_odd_cycle() {
895        // 5-cycle with a heavy chord forces an odd-cycle (blossom) decision.
896        let mut m = WeightedGeneralMatching::new(5);
897        let edges = [
898            (0, 1, 6.0),
899            (1, 2, 6.0),
900            (2, 3, 6.0),
901            (3, 4, 6.0),
902            (4, 0, 6.0),
903            (0, 2, 10.0),
904        ];
905        for &(u, v, w) in &edges {
906            m.add_edge(u, v, w).expect("e");
907        }
908        let res = m.solve().expect("solve");
909        validate(&res, &m.edges(), 5);
910        let brute = brute_max_weight(5, &m.edges());
911        assert!(
912            approx(res.total_weight, brute),
913            "got {} brute {brute}",
914            res.total_weight
915        );
916    }
917
918    #[test]
919    fn matches_brute_force_on_random_graphs() {
920        let mut rng = LcgRng::new(0xC0FFEE);
921        for iter in 0..200 {
922            let n = 2 + (rng.next_u64() as usize % 8); // 2..=9
923            // alternate sparse/dense to exercise both isolated vertices and blossoms
924            let prob = if iter % 2 == 0 { 50 } else { 80 };
925            let mut m = WeightedGeneralMatching::new(n);
926            let mut edges = Vec::new();
927            for u in 0..n {
928                for v in (u + 1)..n {
929                    if rng.next_u64() % 100 < prob {
930                        let w = 1.0 + (rng.next_u64() as usize % 9) as f64; // 1..=9
931                        m.add_edge(u, v, w).expect("e");
932                        edges.push((u, v, w));
933                    }
934                }
935            }
936            let res = m.solve().expect("solve");
937            validate(&res, &m.edges(), n);
938            let brute = brute_max_weight(n, &m.edges());
939            assert!(
940                approx(res.total_weight, brute),
941                "n={n} edges={:?} got {} brute {brute}",
942                edges,
943                res.total_weight
944            );
945        }
946    }
947
948    #[test]
949    fn complete_graph_even_perfect() {
950        // K4 with weights; max weight matching is a perfect matching.
951        let mut m = WeightedGeneralMatching::new(4);
952        m.add_edge(0, 1, 1.0).expect("e");
953        m.add_edge(2, 3, 1.0).expect("e");
954        m.add_edge(0, 2, 5.0).expect("e");
955        m.add_edge(1, 3, 5.0).expect("e");
956        m.add_edge(0, 3, 2.0).expect("e");
957        m.add_edge(1, 2, 2.0).expect("e");
958        let res = m.solve().expect("solve");
959        validate(&res, &m.edges(), 4);
960        // Best: (0,2)+(1,3) = 10.
961        assert!(approx(res.total_weight, 10.0), "got {}", res.total_weight);
962        assert_eq!(res.matched_edges.len(), 2);
963    }
964
965    #[test]
966    fn rejects_bad_edges() {
967        let mut m = WeightedGeneralMatching::new(3);
968        assert!(m.add_edge(0, 5, 1.0).is_err());
969        assert!(m.add_edge(0, 1, f64::NAN).is_err());
970        // self-loop ignored, not an error
971        assert!(m.add_edge(1, 1, 3.0).is_ok());
972        assert_eq!(m.edges().len(), 0);
973    }
974
975    #[test]
976    fn parallel_edges_keep_max() {
977        let mut m = WeightedGeneralMatching::new(2);
978        m.add_edge(0, 1, 2.0).expect("e");
979        m.add_edge(1, 0, 7.0).expect("e");
980        let res = m.solve().expect("solve");
981        assert!(approx(res.total_weight, 7.0));
982    }
983}