Skip to main content

integer_blossom/
lib.rs

1//! Minimum-weight perfect matching on a complete graph (general, non-bipartite).
2//!
3//! Many pairing problems (round-robin/Swiss-style tournament pairing among
4//! them) are matching problems on a *general* graph — any vertex may pair
5//! with any other — so bipartite methods (Hungarian) don't apply and we need
6//! the blossom algorithm. This crate implements the classic O(V³) primal-dual
7//! blossom algorithm for **maximum** weight matching, then reduces the problem
8//! most callers actually care about — a **minimum-weight perfect** matching —
9//! to it:
10//!
11//! On a complete graph with strictly positive edge weights, the maximum-weight
12//! matching is necessarily perfect (any two unmatched vertices are adjacent by a
13//! positive-weight edge, so leaving them unmatched is never optimal). Weighting
14//! each edge `offset - cost`, with `offset` chosen above every cost so all
15//! weights stay ≥ 1, therefore yields the minimum-cost perfect matching.
16//!
17//! Both are exposed: [`max_weight_matching`] solves the general problem — an
18//! arbitrary (possibly sparse, possibly odd-order) graph, leaving a vertex
19//! unmatched where that is optimal — and [`min_weight_perfect_matching`] applies
20//! the reduction above. Both are thin wrappers over one pooled solver, differing
21//! only in how they fill edges and shape the result.
22//!
23//! Weights are generic over [`Weight`] so callers can pick a type just wide
24//! enough for their largest weight — `i32`/`i64` for most instances, `i128`
25//! when more headroom is needed (e.g. to stack large lexicographic
26//! multipliers when scalarizing a multi-criteria cost). Should an instance
27//! ever outgrow `i128`, a fixed-width 256-bit `Weight` impl would be the next
28//! step (a heap-allocated bignum isn't — it'd add allocation to every
29//! arithmetic op in this O(V³) inner loop); benchmarking a non-allocating
30//! 256-bit uint against `i128` on the same instances measured it at only
31//! ~1.7x slower, so the headroom is cheap if it's ever needed.
32//!
33//! The implementation is original — built from the published blossom algorithm,
34//! not ported from any codebase — and is checked against a brute-force oracle in
35//! the tests below.
36
37use std::any::{Any, TypeId};
38use std::cell::RefCell;
39use std::collections::VecDeque;
40
41/// Edge-weight type for the blossom solver: a signed integer wide enough to
42/// hold the caller's largest weight without overflow.
43pub trait Weight:
44    Copy
45    + Ord
46    + std::fmt::Debug
47    + std::ops::Add<Output = Self>
48    + std::ops::Sub<Output = Self>
49    + std::ops::AddAssign
50    + std::ops::SubAssign
51    // `'static` lets the per-thread solver pool key its reusable `Blossom<W>`
52    // buffers by `TypeId`; every integer weight type satisfies it.
53    + 'static
54{
55    const ZERO: Self;
56    const ONE: Self;
57    /// Larger than any edge slack that can arise, but small enough to leave
58    /// headroom against overflow when doubled.
59    fn inf() -> Self;
60    /// `self * 2`, used when doubling dual-variable adjustments.
61    fn double(self) -> Self;
62    /// `self / 2`, used when halving slack to keep duals integral.
63    fn half(self) -> Self;
64}
65
66macro_rules! impl_weight {
67    ($($t:ty),* $(,)?) => {$(
68        impl Weight for $t {
69            const ZERO: Self = 0;
70            const ONE: Self = 1;
71            fn inf() -> Self { <$t>::MAX / 4 }
72            fn double(self) -> Self { self * 2 }
73            fn half(self) -> Self { self / 2 }
74        }
75    )*};
76}
77impl_weight!(i32, i64, i128);
78
79/// A vertex index. The algorithm works in `usize` (loop counters, array indices),
80/// but the two large `O(n²)` tables — the [`Blossom::g`] edge matrix and
81/// [`Blossom::flower_from`] — *store* vertices, and for the field sizes we pair
82/// (hundreds, never near `u32::MAX`) a 4-byte id halves that footprint versus a
83/// `usize`, tightening the hot column scans. Stored as `Vid`, used as `usize`.
84type Vid = u32;
85
86/// One graph edge, carrying the *real* endpoints it stands for. For a super-vertex
87/// (contracted blossom) `b`, `g[b][x]` records the best underlying real edge, so
88/// `u`/`v` are always real-vertex indices even when the slot is `g[b][x]`.
89#[derive(Clone, Copy)]
90struct Edge<W> {
91    u: Vid,
92    v: Vid,
93    w: W,
94}
95
96/// Working state of the blossom algorithm. Vertices are 1-indexed; indices
97/// `1..=n` are real players and `n+1..=2n` are contracted blossoms.
98struct Blossom<W> {
99    n: usize,
100    n_x: usize,
101    /// The `sz × sz` edge matrix, row-major in a single allocation: entry `(u, v)`
102    /// is at `u * stride + v` (see [`Blossom::g`]). One block, rather than a
103    /// `Vec<Vec>`, so the algorithm's column scans (`for u { g[u][x] }` in
104    /// `set_slack`/`update_slack`, the hot path) stay in one strided region the
105    /// prefetcher can follow instead of chasing `n` separate heap rows.
106    g: Vec<Edge<W>>,
107    /// Row stride of `g` — the allocated width `2 * n_cap + 1` for the largest
108    /// instance seen, so a reused buffer keeps a consistent layout.
109    stride: usize,
110    lab: Vec<W>,
111    mate: Vec<usize>,
112    slack: Vec<usize>,
113    st: Vec<usize>,
114    pa: Vec<usize>,
115    /// The `sz × (n+1)` "which member does row `u` reach vertex `v` through"
116    /// matrix, row-major in a single allocation (entry `(u, v)` at
117    /// `u * ff_stride + v`), for the same locality reason as [`Blossom::g`].
118    flower_from: Vec<Vid>,
119    /// Row stride of `flower_from` — the allocated width `n_cap + 1` for the
120    /// largest instance seen.
121    ff_stride: usize,
122    s: Vec<i32>,
123    vis: Vec<usize>,
124    flower: Vec<Vec<usize>>,
125    q: VecDeque<usize>,
126    t: usize,
127}
128
129impl<W: Weight> Blossom<W> {
130    fn new(n: usize) -> Self {
131        let sz = 2 * n + 1;
132        let nil_edge = Edge {
133            u: 0,
134            v: 0,
135            w: W::ZERO,
136        };
137        Blossom {
138            n,
139            n_x: n,
140            g: vec![nil_edge; sz * sz],
141            stride: sz,
142            lab: vec![W::ZERO; sz],
143            mate: vec![0; sz],
144            slack: vec![0; sz],
145            st: vec![0; sz],
146            pa: vec![0; sz],
147            flower_from: vec![0; sz * (n + 1)],
148            ff_stride: n + 1,
149            s: vec![-1; sz],
150            vis: vec![0; sz],
151            flower: vec![Vec::new(); sz],
152            q: VecDeque::new(),
153            t: 0,
154        }
155    }
156
157    /// Prepare a (possibly reused) solver for an `n`-vertex instance, growing the
158    /// working buffers if this instance is larger than any this solver has seen.
159    ///
160    /// No stale data needs clearing: `solve`/`matching` re-initialize every piece
161    /// of live state within `1..=2n` each run, both entry points call `set_edge`
162    /// for every vertex pair (`max_weight_matching` clamping an absent edge to a
163    /// zero weight) so every real edge slot is overwritten, and a super-vertex's
164    /// row/column is zeroed when its blossom is formed — so a buffer left over
165    /// from an earlier (larger or smaller) instance is correct as long as it is
166    /// big enough. Growth only ever extends the buffers (indices `1..=2n` are all
167    /// a smaller `n` could have touched), never truncates.
168    fn reset(&mut self, n: usize) {
169        self.n = n;
170        self.n_x = n;
171        let sz = 2 * n + 1;
172        if sz > self.stride {
173            // A larger instance than any before: reallocate the buffers wide enough
174            // (the old contents are stale and would be overwritten anyway, so there
175            // is nothing to copy). `g` is laid out at the new stride from here on.
176            let nil = Edge {
177                u: 0,
178                v: 0,
179                w: W::ZERO,
180            };
181            self.stride = sz;
182            self.g = vec![nil; sz * sz];
183            self.flower_from = vec![0; sz * (n + 1)];
184            self.ff_stride = n + 1;
185            self.lab.resize(sz, W::ZERO);
186            self.mate.resize(sz, 0);
187            self.slack.resize(sz, 0);
188            self.st.resize(sz, 0);
189            self.pa.resize(sz, 0);
190            self.s.resize(sz, -1);
191            self.vis.resize(sz, 0);
192            self.flower.resize(sz, Vec::new());
193        }
194    }
195
196    /// Edge `(u, v)` of the flattened matrix (returned by value — `Edge` is `Copy`).
197    #[inline]
198    fn g(&self, u: usize, v: usize) -> Edge<W> {
199        self.g[u * self.stride + v]
200    }
201
202    /// Mutable edge `(u, v)`, for the few sites that overwrite an edge or zero its
203    /// weight in place.
204    #[inline]
205    fn g_mut(&mut self, u: usize, v: usize) -> &mut Edge<W> {
206        &mut self.g[u * self.stride + v]
207    }
208
209    /// Entry `(u, v)` of the flattened `flower_from` matrix.
210    #[inline]
211    fn flower_from(&self, u: usize, v: usize) -> usize {
212        self.flower_from[u * self.ff_stride + v] as usize
213    }
214
215    /// Set entry `(u, v)` of `flower_from` (narrowing the stored vertex to [`Vid`]).
216    #[inline]
217    fn set_flower_from(&mut self, u: usize, v: usize, val: usize) {
218        self.flower_from[u * self.ff_stride + v] = val as Vid;
219    }
220
221    fn set_edge(&mut self, u: usize, v: usize, w: W) {
222        let (u, v) = (u as Vid, v as Vid);
223        *self.g_mut(u as usize, v as usize) = Edge { u, v, w };
224        // Reverse orientation: endpoints swapped (field-named, not positional).
225        *self.g_mut(v as usize, u as usize) = Edge { u: v, v: u, w };
226    }
227
228    /// Reduced cost (slack) of an edge; zero means the edge is tight.
229    fn e_delta(&self, e: Edge<W>) -> W {
230        self.lab[e.u as usize] + self.lab[e.v as usize] - e.w.double()
231    }
232
233    fn update_slack(&mut self, u: usize, x: usize) {
234        if self.slack[x] == 0 || self.e_delta(self.g(u, x)) < self.e_delta(self.g(self.slack[x], x))
235        {
236            self.slack[x] = u;
237        }
238    }
239
240    fn set_slack(&mut self, x: usize) {
241        self.slack[x] = 0;
242        for u in 1..=self.n {
243            if self.g(u, x).w > W::ZERO && self.st[u] != x && self.s[self.st[u]] == 0 {
244                self.update_slack(u, x);
245            }
246        }
247    }
248
249    fn q_push(&mut self, x: usize) {
250        if x <= self.n {
251            self.q.push_back(x);
252        } else {
253            // Recursing into a child never mutates `flower[x]` itself, so index it
254            // in place rather than cloning the whole cycle each call.
255            let mut i = 0;
256            while i < self.flower[x].len() {
257                let c = self.flower[x][i];
258                self.q_push(c);
259                i += 1;
260            }
261        }
262    }
263
264    fn set_st(&mut self, x: usize, b: usize) {
265        self.st[x] = b;
266        if x > self.n {
267            let mut i = 0;
268            while i < self.flower[x].len() {
269                let c = self.flower[x][i];
270                self.set_st(c, b);
271                i += 1;
272            }
273        }
274    }
275
276    /// Position of `xr` within blossom `b`'s cycle, normalized to be even by
277    /// reversing the tail if needed (so a matched alternating walk starts right).
278    fn get_pr(&mut self, b: usize, xr: usize) -> usize {
279        let pr = self.flower[b].iter().position(|&x| x == xr).unwrap();
280        if pr % 2 == 1 {
281            let len = self.flower[b].len();
282            self.flower[b][1..].reverse();
283            len - pr
284        } else {
285            pr
286        }
287    }
288
289    fn set_match(&mut self, u: usize, v: usize) {
290        self.mate[u] = self.g(u, v).v as usize;
291        if u > self.n {
292            let e = self.g(u, v);
293            let xr = self.flower_from(u, e.u as usize);
294            let pr = self.get_pr(u, xr);
295            // The recursive `set_match` on a child touches only that child's cycle,
296            // so `flower[u]` is stable until the `rotate_left` below — index it in
297            // place instead of cloning.
298            let mut i = 0;
299            while i < pr {
300                let a = self.flower[u][i];
301                let b = self.flower[u][i ^ 1];
302                self.set_match(a, b);
303                i += 1;
304            }
305            self.set_match(xr, v);
306            self.flower[u].rotate_left(pr);
307        }
308    }
309
310    fn augment(&mut self, mut u: usize, mut v: usize) {
311        loop {
312            let xnv = self.st[self.mate[u]];
313            self.set_match(u, v);
314            if xnv == 0 {
315                return;
316            }
317            let next_u = self.st[self.pa[xnv]];
318            self.set_match(xnv, next_u);
319            u = next_u;
320            v = xnv;
321        }
322    }
323
324    fn get_lca(&mut self, mut u: usize, mut v: usize) -> usize {
325        self.t += 1;
326        loop {
327            if u == 0 && v == 0 {
328                return 0;
329            }
330            if u != 0 {
331                if self.vis[u] == self.t {
332                    return u;
333                }
334                self.vis[u] = self.t;
335                let m = self.st[self.mate[u]];
336                u = if m != 0 { self.st[self.pa[m]] } else { 0 };
337            }
338            std::mem::swap(&mut u, &mut v);
339        }
340    }
341
342    fn add_blossom(&mut self, u: usize, lca: usize, v: usize) {
343        let mut b = self.n + 1;
344        while b <= self.n_x && self.st[b] != 0 {
345            b += 1;
346        }
347        if b > self.n_x {
348            self.n_x += 1;
349        }
350        self.lab[b] = W::ZERO;
351        self.s[b] = 0;
352        self.mate[b] = self.mate[lca];
353        self.flower[b].clear();
354        self.flower[b].push(lca);
355
356        let mut x = u;
357        while x != lca {
358            let y = self.st[self.mate[x]];
359            self.flower[b].push(x);
360            self.flower[b].push(y);
361            self.q_push(y);
362            x = self.st[self.pa[y]];
363        }
364        self.flower[b][1..].reverse();
365        let mut x = v;
366        while x != lca {
367            let y = self.st[self.mate[x]];
368            self.flower[b].push(x);
369            self.flower[b].push(y);
370            self.q_push(y);
371            x = self.st[self.pa[y]];
372        }
373
374        self.set_st(b, b);
375        for x in 1..=self.n_x {
376            self.g_mut(b, x).w = W::ZERO;
377            self.g_mut(x, b).w = W::ZERO;
378        }
379        for x in 1..=self.n {
380            self.set_flower_from(b, x, 0);
381        }
382        // `b` is a fresh index distinct from every member `xs`, so writing row/col
383        // `b` never disturbs the `xs` rows we read — the cycle is stable, index it
384        // in place rather than cloning.
385        let mut mi = 0;
386        while mi < self.flower[b].len() {
387            let xs = self.flower[b][mi];
388            for x in 1..=self.n_x {
389                let gxsx = self.g(xs, x);
390                let gxxs = self.g(x, xs);
391                let gbx = self.g(b, x);
392                if gbx.w == W::ZERO || self.e_delta(gxsx) < self.e_delta(gbx) {
393                    *self.g_mut(b, x) = gxsx;
394                    *self.g_mut(x, b) = gxxs;
395                }
396            }
397            for x in 1..=self.n {
398                if self.flower_from(xs, x) != 0 {
399                    self.set_flower_from(b, x, xs);
400                }
401            }
402            mi += 1;
403        }
404        self.set_slack(b);
405    }
406
407    fn expand_blossom(&mut self, b: usize) {
408        // `set_st` only descends into each member's own sub-cycle, so `flower[b]`
409        // is stable here — index it rather than cloning.
410        let mut mi = 0;
411        while mi < self.flower[b].len() {
412            let m = self.flower[b][mi];
413            self.set_st(m, m);
414            mi += 1;
415        }
416        let xr = self.flower_from(b, self.g(b, self.pa[b]).u as usize);
417        // `get_pr` may reverse `flower[b][1..]`; every index below reads it after,
418        // and `set_slack`/`q_push` never mutate it, so no clone is needed.
419        let pr = self.get_pr(b, xr);
420        let mut i = 0;
421        while i < pr {
422            let xs = self.flower[b][i];
423            let xns = self.flower[b][i + 1];
424            self.pa[xs] = self.g(xns, xs).u as usize;
425            self.s[xs] = 1;
426            self.s[xns] = 0;
427            self.slack[xs] = 0;
428            self.set_slack(xns);
429            self.q_push(xns);
430            i += 2;
431        }
432        self.s[xr] = 1;
433        self.pa[xr] = self.pa[b];
434        let mut idx = pr + 1;
435        while idx < self.flower[b].len() {
436            let xs = self.flower[b][idx];
437            self.s[xs] = -1;
438            self.set_slack(xs);
439            idx += 1;
440        }
441        self.st[b] = 0;
442    }
443
444    fn on_found_edge(&mut self, e: Edge<W>) -> bool {
445        let u = self.st[e.u as usize];
446        let v = self.st[e.v as usize];
447        if self.s[v] == -1 {
448            self.pa[v] = e.u as usize;
449            self.s[v] = 1;
450            let nu = self.st[self.mate[v]];
451            self.slack[v] = 0;
452            self.slack[nu] = 0;
453            self.s[nu] = 0;
454            self.q_push(nu);
455        } else if self.s[v] == 0 {
456            let lca = self.get_lca(u, v);
457            if lca == 0 {
458                self.augment(u, v);
459                self.augment(v, u);
460                return true;
461            } else {
462                self.add_blossom(u, lca, v);
463            }
464        }
465        false
466    }
467
468    /// One phase: grow alternating trees, adjusting duals, until an augmenting
469    /// path is found (returns `true`, matching grew by one edge) or no further
470    /// improvement is possible (returns `false`).
471    fn matching(&mut self) -> bool {
472        for i in 1..=self.n_x {
473            self.s[i] = -1;
474            self.slack[i] = 0;
475        }
476        self.q.clear();
477        for x in 1..=self.n_x {
478            if self.st[x] == x && self.mate[x] == 0 {
479                self.pa[x] = 0;
480                self.s[x] = 0;
481                self.q_push(x);
482            }
483        }
484        if self.q.is_empty() {
485            return false;
486        }
487        loop {
488            while let Some(u) = self.q.pop_front() {
489                if self.s[self.st[u]] == 1 {
490                    continue;
491                }
492                for v in 1..=self.n {
493                    if self.g(u, v).w > W::ZERO && self.st[u] != self.st[v] {
494                        if self.e_delta(self.g(u, v)) == W::ZERO {
495                            if self.on_found_edge(self.g(u, v)) {
496                                return true;
497                            }
498                        } else {
499                            let x = self.st[v];
500                            self.update_slack(u, x);
501                        }
502                    }
503                }
504            }
505            let mut d = W::inf();
506            for b in (self.n + 1)..=self.n_x {
507                if self.st[b] == b && self.s[b] == 1 {
508                    d = d.min(self.lab[b].half());
509                }
510            }
511            for x in 1..=self.n_x {
512                if self.st[x] == x && self.slack[x] != 0 {
513                    let delta = self.e_delta(self.g(self.slack[x], x));
514                    if self.s[x] == -1 {
515                        d = d.min(delta);
516                    } else if self.s[x] == 0 {
517                        d = d.min(delta.half());
518                    }
519                }
520            }
521            for u in 1..=self.n {
522                match self.s[self.st[u]] {
523                    0 => {
524                        if self.lab[u] <= d {
525                            return false;
526                        }
527                        self.lab[u] -= d;
528                    }
529                    1 => self.lab[u] += d,
530                    _ => {}
531                }
532            }
533            for b in (self.n + 1)..=self.n_x {
534                if self.st[b] == b {
535                    if self.s[b] == 0 {
536                        self.lab[b] += d.double();
537                    } else if self.s[b] == 1 {
538                        self.lab[b] -= d.double();
539                    }
540                }
541            }
542            self.q.clear();
543            for x in 1..=self.n_x {
544                if self.st[x] == x
545                    && self.slack[x] != 0
546                    && self.st[self.slack[x]] != x
547                    && self.e_delta(self.g(self.slack[x], x)) == W::ZERO
548                    && self.on_found_edge(self.g(self.slack[x], x))
549                {
550                    return true;
551                }
552            }
553            for b in (self.n + 1)..=self.n_x {
554                if self.st[b] == b && self.s[b] == 1 && self.lab[b] == W::ZERO {
555                    self.expand_blossom(b);
556                }
557            }
558        }
559    }
560
561    fn solve(&mut self) {
562        for u in 1..=self.n {
563            self.mate[u] = 0;
564        }
565        self.n_x = self.n;
566        for u in 0..=self.n {
567            self.st[u] = u;
568            self.flower[u].clear();
569        }
570        for b in (self.n + 1)..(2 * self.n + 1) {
571            self.st[b] = 0;
572            self.flower[b].clear();
573        }
574        let mut w_max = W::ZERO;
575        for u in 1..=self.n {
576            for v in 1..=self.n {
577                self.set_flower_from(u, v, if u == v { u } else { 0 });
578                if self.g(u, v).w > w_max {
579                    w_max = self.g(u, v).w;
580                }
581            }
582        }
583        for u in 1..=self.n {
584            self.lab[u] = w_max;
585        }
586        while self.matching() {}
587    }
588}
589
590/// Solve one `n`-vertex instance on the per-thread pooled solver: `set_edges`
591/// writes the graph (it must call `set_edge` for *every* vertex pair so no stale
592/// pooled edge survives — see [`Blossom::reset`]), then each vertex's raw mate is
593/// passed through `map_mate` to build the result in a single allocation.
594///
595/// Both public entry points funnel through here so they share the pool and its
596/// buffer reuse; they differ only in how they fill edges and shape the output.
597/// `map_mate` receives the raw 1-indexed partner (`0` meaning unmatched).
598fn solve_pooled<W: Weight, T>(
599    n: usize,
600    set_edges: impl FnOnce(&mut Blossom<W>),
601    map_mate: impl Fn(usize) -> T,
602) -> Vec<T> {
603    POOL.with(|pool| {
604        let mut pool = pool.borrow_mut();
605        let bl = pool.get::<W>(n);
606        bl.reset(n);
607        set_edges(bl);
608        bl.solve();
609        (1..=n).map(|u| map_mate(bl.mate[u])).collect()
610    })
611}
612
613/// Compute a maximum-total-weight matching of the `n` vertices, where `weight` is
614/// the **row-major** `n × n` weight matrix — the weight of the edge between `i`
615/// and `j` is `weight[i * n + j]`. Returns `mate`, where `mate[i]` is `Some(j)` if
616/// `i` is matched to `j` and `None` if `i` is left unmatched.
617///
618/// The graph need not be complete and `n` need not be even: a vertex is left
619/// unmatched whenever matching it cannot increase the total. An entry that is
620/// **zero or negative** is treated as *no usable edge* — such a pair is never
621/// matched (a max-weight matching would never pick a non-positive edge anyway) —
622/// so a caller encodes a sparse graph by leaving absent edges at zero.
623///
624/// `weight` must have exactly `n * n` entries. Only the strict **upper triangle**
625/// (`i < j`) is read — the weight is taken as symmetric, so the diagonal and the
626/// lower triangle are ignored and a caller may leave them unset. The matrix is a
627/// flat slice, not `&[Vec<_>]`, so a caller building it (and the solver reading
628/// it) touches one contiguous allocation rather than `n` rows.
629pub fn max_weight_matching<W: Weight>(weight: &[W], n: usize) -> Vec<Option<usize>> {
630    assert_eq!(weight.len(), n * n, "weight must be a row-major n×n matrix");
631    if n == 0 {
632        return Vec::new();
633    }
634
635    solve_pooled(
636        n,
637        |bl| {
638            for i in 0..n {
639                for j in (i + 1)..n {
640                    // The solver reads a non-positive weight as "no edge"; clamp to
641                    // zero so every edge slot is still overwritten — a reused pool
642                    // buffer may hold a previous instance's edges (see `reset`).
643                    let w = weight[i * n + j];
644                    bl.set_edge(i + 1, j + 1, if w > W::ZERO { w } else { W::ZERO });
645                }
646            }
647        },
648        // Raw mate `0` means unmatched; otherwise de-bias to a 0-indexed partner.
649        |m| (m != 0).then(|| m - 1),
650    )
651}
652
653/// Compute a minimum-total-cost **perfect** matching of the `n` vertices, where
654/// `cost` is the **row-major** `n × n` cost matrix — the cost of pairing `i` with
655/// `j` is `cost[i * n + j]`. Returns `mate`, where `mate[i]` is the partner of
656/// vertex `i`.
657///
658/// `n` must be even (a perfect matching is otherwise impossible) and `cost` must
659/// have exactly `n * n` entries. Only the strict **upper triangle** (`i < j`) is
660/// read — the cost is taken as symmetric, so the diagonal and the lower triangle
661/// are ignored and a caller may leave them unset. Costs must be non-negative. On
662/// the complete graph every vertex is pairable, so a perfect matching always
663/// exists.
664///
665/// The matrix is a flat slice, not `&[Vec<_>]`, so a caller building it touches
666/// one contiguous allocation rather than `n` rows.
667pub fn min_weight_perfect_matching<W: Weight>(cost: &[W], n: usize) -> Vec<usize> {
668    assert_eq!(cost.len(), n * n, "cost must be a row-major n×n matrix");
669    assert!(
670        n.is_multiple_of(2),
671        "a perfect matching needs an even vertex count"
672    );
673    if n == 0 {
674        return Vec::new();
675    }
676
677    // Reduce min-cost-perfect to max-weight: weight = offset - cost, with offset
678    // above every cost so all weights are ≥ 1. Every edge of the complete graph is
679    // then positive, so the maximum-weight matching is necessarily perfect (any
680    // two unmatched vertices could be joined by a positive edge) — and, since its
681    // total weight is `offset·(n/2) − total_cost`, maximizing weight minimizes
682    // cost. So the max-weight matching is exactly the min-cost perfect one. The
683    // `offset - cost` edges are fed straight to the solver rather than through a
684    // materialized weight matrix, so no `n²` buffer is allocated per call.
685    let mut max_cost = W::ZERO;
686    for i in 0..n {
687        for j in (i + 1)..n {
688            let c = cost[i * n + j];
689            if c > max_cost {
690                max_cost = c;
691            }
692        }
693    }
694    let offset = max_cost + W::ONE;
695
696    solve_pooled(
697        n,
698        |bl| {
699            for i in 0..n {
700                for j in (i + 1)..n {
701                    bl.set_edge(i + 1, j + 1, offset - cost[i * n + j]);
702                }
703            }
704        },
705        // Every vertex is matched (the matching is perfect), so the raw mate is
706        // always ≥ 1; de-bias to a 0-indexed partner.
707        |m| m - 1,
708    )
709}
710
711thread_local! {
712    /// Per-thread reuse of the solver's O(n²) working buffers (see [`Pool`]).
713    static POOL: RefCell<Pool> = const { RefCell::new(Pool::new()) };
714}
715
716/// A per-thread cache of reusable [`Blossom`] solvers, one per weight type. The
717/// solver's buffers are the dominant allocation — `g` alone is `(2n+1)²` edges —
718/// and a caller like osp-sim runs thousands of same-sized matchings per thread,
719/// so keeping the buffers and merely [`Blossom::reset`]ting them between calls
720/// turns those per-call allocations into one per thread.
721///
722/// It is keyed by [`TypeId`] because the solver is generic over `W` while a
723/// thread-local is not; with only the handful of integer weight types in use a
724/// linear scan of the slots is cheaper than a map.
725struct Pool {
726    slots: Vec<(TypeId, Box<dyn Any>)>,
727}
728
729impl Pool {
730    const fn new() -> Self {
731        Pool { slots: Vec::new() }
732    }
733
734    /// The reusable solver for weight type `W`, created (sized for `n`) on first
735    /// use for this type on this thread.
736    fn get<W: Weight>(&mut self, n: usize) -> &mut Blossom<W> {
737        let tid = TypeId::of::<W>();
738        let idx = match self.slots.iter().position(|(t, _)| *t == tid) {
739            Some(i) => i,
740            None => {
741                self.slots.push((tid, Box::new(Blossom::<W>::new(n))));
742                self.slots.len() - 1
743            }
744        };
745        self.slots[idx]
746            .1
747            .downcast_mut::<Blossom<W>>()
748            .expect("each slot holds the Blossom<W> its TypeId keys")
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755
756    /// Flatten a square `Vec<Vec<W>>` into the row-major slice the solver takes.
757    /// The tests keep the readable nested form (and hand it to the oracle as-is);
758    /// this bridges to the flat API at the call.
759    fn flat<W: Copy>(cost: &[Vec<W>]) -> Vec<W> {
760        cost.iter().flatten().copied().collect()
761    }
762
763    /// Reference: minimum total cost over all perfect matchings, by exhaustive
764    /// recursion. Only usable for tiny `n`, which is exactly what we test against.
765    fn brute_min_cost(cost: &[Vec<i128>]) -> i128 {
766        let n = cost.len();
767        let mut used = vec![false; n];
768        fn rec(cost: &[Vec<i128>], used: &mut Vec<bool>, matched: usize, n: usize) -> i128 {
769            if matched == n {
770                return 0;
771            }
772            // first unmatched vertex
773            let i = (0..n).find(|&i| !used[i]).unwrap();
774            used[i] = true;
775            let mut best = i128::inf();
776            for j in (i + 1)..n {
777                if !used[j] {
778                    used[j] = true;
779                    let sub = rec(cost, used, matched + 2, n);
780                    if sub < i128::inf() {
781                        best = best.min(cost[i][j] + sub);
782                    }
783                    used[j] = false;
784                }
785            }
786            used[i] = false;
787            best
788        }
789        rec(cost, &mut used, 0, n)
790    }
791
792    fn total_of(cost: &[Vec<i128>], mate: &[usize]) -> i128 {
793        let n = cost.len();
794        let mut t = 0;
795        for i in 0..n {
796            assert_ne!(mate[i], i, "vertex matched to itself");
797            assert_eq!(mate[mate[i]], i, "matching is not a valid involution");
798            if i < mate[i] {
799                t += cost[i][mate[i]];
800            }
801        }
802        t
803    }
804
805    /// Reference: maximum total weight over all matchings (not necessarily
806    /// perfect), by exhaustive recursion. Mirrors `max_weight_matching`'s
807    /// semantics — a non-positive weight is "no edge", and a vertex may be left
808    /// unmatched. Only usable for tiny `n`.
809    fn brute_max_weight(w: &[Vec<i128>]) -> i128 {
810        let n = w.len();
811        fn rec(w: &[Vec<i128>], used: &mut Vec<bool>, n: usize) -> i128 {
812            let i = match (0..n).find(|&i| !used[i]) {
813                Some(i) => i,
814                None => return 0,
815            };
816            used[i] = true;
817            // Option 1: leave `i` unmatched.
818            let mut best = rec(w, used, n);
819            // Option 2: match `i` to any later usable (positive-weight) partner.
820            for j in (i + 1)..n {
821                if !used[j] && w[i][j] > 0 {
822                    used[j] = true;
823                    best = best.max(w[i][j] + rec(w, used, n));
824                    used[j] = false;
825                }
826            }
827            used[i] = false;
828            best
829        }
830        rec(w, &mut vec![false; n], n)
831    }
832
833    /// Total weight of `mate`, checking it is a valid matching that only uses
834    /// positive-weight edges.
835    fn total_weight(w: &[Vec<i128>], mate: &[Option<usize>]) -> i128 {
836        let n = w.len();
837        let mut t = 0;
838        for i in 0..n {
839            if let Some(j) = mate[i] {
840                assert_ne!(j, i, "vertex matched to itself");
841                assert_eq!(mate[j], Some(i), "matching is not a valid involution");
842                assert!(w[i][j] > 0, "matched a non-positive (absent) edge {i}-{j}");
843                if i < j {
844                    t += w[i][j];
845                }
846            }
847        }
848        t
849    }
850
851    #[test]
852    fn trivial_pair() {
853        let cost = vec![vec![0, 7], vec![7, 0]];
854        let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
855        assert_eq!(mate, vec![1, 0]);
856    }
857
858    #[test]
859    fn reuses_buffers_across_shrinking_sizes() {
860        // The per-thread solver pool keeps a buffer sized for the largest instance
861        // seen, so a later *smaller* solve runs on a buffer holding a bigger
862        // instance's stale edges (some now in the super-vertex index range). This
863        // is the case the ascending brute-force test never hits. Solve a large
864        // instance to grow-and-dirty the buffer, then check small instances — which
865        // now reuse it — against the exhaustive oracle.
866        let mut seed: u64 = 0x243F6A8885A308D3;
867        let mut next = || {
868            seed ^= seed << 13;
869            seed ^= seed >> 7;
870            seed ^= seed << 17;
871            seed
872        };
873        let mut random_cost = |n: usize| {
874            let mut cost = vec![vec![0i128; n]; n];
875            #[allow(clippy::needless_range_loop)]
876            for i in 0..n {
877                for j in (i + 1)..n {
878                    let c = (next() % 1000) as i128;
879                    cost[i][j] = c;
880                    cost[j][i] = c;
881                }
882            }
883            cost
884        };
885
886        for _ in 0..50 {
887            // Dirty the buffer with a large instance, then a small one that reuses
888            // it — and interleave sizes so the shrink path is hit repeatedly.
889            let _ = min_weight_perfect_matching(&flat(&random_cost(120)), 120);
890            for &n in &[2usize, 4, 6, 8, 10, 4, 8, 2] {
891                let cost = random_cost(n);
892                let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
893                assert_eq!(
894                    total_of(&cost, &mate),
895                    brute_min_cost(&cost),
896                    "reused buffer gave a suboptimal matching at n={n}"
897                );
898            }
899        }
900    }
901
902    #[test]
903    fn picks_cheaper_of_two_pairings() {
904        // 4 vertices; pairing {0-1, 2-3} costs 1+1=2, {0-2,1-3} costs 10+10=20,
905        // {0-3,1-2} costs 10+10=20. Optimal keeps the cheap edges.
906        let cost = vec![
907            vec![0, 1, 10, 10],
908            vec![1, 0, 10, 10],
909            vec![10, 10, 0, 1],
910            vec![10, 10, 1, 0],
911        ];
912        let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
913        assert_eq!(total_of(&cost, &mate), 2);
914        assert_eq!(mate[0], 1);
915        assert_eq!(mate[2], 3);
916    }
917
918    #[test]
919    fn matches_brute_force_on_random_instances() {
920        // Small deterministic LCG so the test is reproducible without a dep.
921        let mut seed: u64 = 0x9E3779B97F4A7C15;
922        let mut next = || {
923            seed ^= seed << 13;
924            seed ^= seed >> 7;
925            seed ^= seed << 17;
926            seed
927        };
928
929        for &n in &[2usize, 4, 6, 8, 10] {
930            for _ in 0..200 {
931                let mut cost = vec![vec![0i128; n]; n];
932                #[allow(clippy::needless_range_loop)]
933                for i in 0..n {
934                    for j in (i + 1)..n {
935                        let c = (next() % 1000) as i128;
936                        cost[i][j] = c;
937                        cost[j][i] = c;
938                    }
939                }
940                let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
941                let got = total_of(&cost, &mate);
942                let want = brute_min_cost(&cost);
943                assert_eq!(got, want, "n={n}, cost={cost:?}, mate={mate:?}");
944            }
945        }
946    }
947
948    #[test]
949    fn handles_large_lexicographic_weights() {
950        // Weights spanning the multiplier ladder's magnitude must not overflow or
951        // lose the ordering: the huge-cost edge (0-1) must be avoided.
952        const BIG: i128 = 1_000_000_000_000_000_000_000_000; // 1e24
953        let cost = vec![
954            vec![0, BIG, 5, 3],
955            vec![BIG, 0, 3, 5],
956            vec![5, 3, 0, BIG],
957            vec![3, 5, BIG, 0],
958        ];
959        let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
960        assert_eq!(brute_min_cost(&cost), total_of(&cost, &mate));
961        assert_ne!(
962            mate[0], 1,
963            "should not pair the two most-penalized vertices"
964        );
965    }
966
967    #[test]
968    fn max_weight_leaves_a_vertex_unmatched() {
969        // A triangle of positive edges (odd order): the best matching takes the
970        // single heaviest edge and leaves the third vertex unmatched.
971        let w = vec![vec![0, 5, 3], vec![5, 0, 4], vec![3, 4, 0]];
972        let mate = max_weight_matching(&flat(&w), w.len());
973        assert_eq!(total_weight(&w, &mate), 5);
974        assert_eq!(mate[0], Some(1));
975        assert_eq!(mate[1], Some(0));
976        assert_eq!(mate[2], None);
977    }
978
979    #[test]
980    fn max_weight_respects_absent_edges() {
981        // Only two positive edges exist; a zero weight is "no edge" and must never
982        // be matched. Best matching is the pair of disjoint present edges.
983        let w = vec![
984            vec![0, 7, 0, 0],
985            vec![7, 0, 0, 0],
986            vec![0, 0, 0, 9],
987            vec![0, 0, 9, 0],
988        ];
989        let mate = max_weight_matching(&flat(&w), w.len());
990        assert_eq!(total_weight(&w, &mate), 16);
991        assert_eq!(mate[0], Some(1));
992        assert_eq!(mate[2], Some(3));
993    }
994
995    #[test]
996    fn max_weight_matches_brute_force_on_sparse_instances() {
997        // Random instances with many zero weights (a sparse graph) and odd as well
998        // as even orders, checked against the exhaustive max-weight oracle.
999        let mut seed: u64 = 0x2545F4914F6CDD1D;
1000        let mut next = || {
1001            seed ^= seed << 13;
1002            seed ^= seed >> 7;
1003            seed ^= seed << 17;
1004            seed
1005        };
1006
1007        for &n in &[1usize, 2, 3, 4, 5, 6, 7, 8] {
1008            for _ in 0..300 {
1009                let mut w = vec![vec![0i128; n]; n];
1010                #[allow(clippy::needless_range_loop)]
1011                for i in 0..n {
1012                    for j in (i + 1)..n {
1013                        // ~1/3 of pairs are absent (weight 0); the rest 1..=1000.
1014                        let c = match next() % 3 {
1015                            0 => 0,
1016                            _ => (next() % 1000 + 1) as i128,
1017                        };
1018                        w[i][j] = c;
1019                        w[j][i] = c;
1020                    }
1021                }
1022                let mate = max_weight_matching(&flat(&w), n);
1023                assert_eq!(
1024                    total_weight(&w, &mate),
1025                    brute_max_weight(&w),
1026                    "n={n}, w={w:?}, mate={mate:?}"
1027                );
1028            }
1029        }
1030    }
1031
1032    #[test]
1033    fn works_with_a_narrower_weight_type() {
1034        // Same instance as `picks_cheaper_of_two_pairings`, but run with `i64`
1035        // weights to confirm the solver isn't secretly tied to `i128`.
1036        let cost: Vec<Vec<i64>> = vec![
1037            vec![0, 1, 10, 10],
1038            vec![1, 0, 10, 10],
1039            vec![10, 10, 0, 1],
1040            vec![10, 10, 1, 0],
1041        ];
1042        let mate = min_weight_perfect_matching(&flat(&cost), cost.len());
1043        assert_eq!(mate[0], 1);
1044        assert_eq!(mate[2], 3);
1045    }
1046
1047    #[test]
1048    fn metamorphic_forbidding_edges_on_large_instances() {
1049        // Brute force can't reach these sizes, so we check the solver against
1050        // itself with two metamorphic relations. Forbid an edge (simulated by a
1051        // penalty cost that dwarfs any real matching, so the optimizer avoids
1052        // it whenever an alternative exists — always, on a complete graph) and
1053        // re-solve:
1054        //   * forbidding an edge the optimum *uses* can only make things worse
1055        //     or equal — the feasible set shrank                (new_cost ≥ base)
1056        //   * forbidding an edge the optimum *doesn't* use leaves the optimum
1057        //     untouched — the old solution is still available   (new_cost = base)
1058        // The equality case catches suboptimality in *either* run; the ≥ case
1059        // catches a first run that missed a better edge-avoiding matching.
1060        let mut seed: u64 = 0xD1B54A32D192ED03;
1061        let mut next = || {
1062            seed ^= seed << 13;
1063            seed ^= seed >> 7;
1064            seed ^= seed << 17;
1065            seed
1066        };
1067
1068        // Real costs are < 1000, so a matching's total is < (n/2)·1000 ≪ 1e9;
1069        // a forbidden edge alone costs 1e9, so any alternative is preferred. And
1070        // 1e9 sits far below i128's headroom (inf() = MAX/4 ≈ 4e37), so doubling
1071        // weights in `e_delta` never overflows.
1072        const PENALTY: i128 = 1_000_000_000;
1073
1074        for &n in &[50usize, 100, 200] {
1075            let mut cost = vec![vec![0i128; n]; n];
1076            #[allow(clippy::needless_range_loop)]
1077            for i in 0..n {
1078                for j in (i + 1)..n {
1079                    let c = (next() % 1000) as i128;
1080                    cost[i][j] = c;
1081                    cost[j][i] = c;
1082                }
1083            }
1084
1085            let base_mate = min_weight_perfect_matching(&flat(&cost), cost.len());
1086            let base_cost = total_of(&cost, &base_mate);
1087
1088            // Forbid a handful of edges the solution uses: cost must not improve.
1089            let solution_edges: Vec<(usize, usize)> = (0..n)
1090                .filter(|&i| i < base_mate[i])
1091                .map(|i| (i, base_mate[i]))
1092                .collect();
1093            for &(i, j) in solution_edges.iter().take(5) {
1094                let mut c2 = cost.clone();
1095                c2[i][j] = PENALTY;
1096                c2[j][i] = PENALTY;
1097                let m2 = min_weight_perfect_matching(&flat(&c2), c2.len());
1098                assert_ne!(
1099                    m2[i], j,
1100                    "n={n}: forbidden solution edge {i}-{j} was still used"
1101                );
1102                // Score with the *original* costs; the forbidden edge is unused,
1103                // so the penalty never enters the total.
1104                let new_cost = total_of(&cost, &m2);
1105                assert!(
1106                    new_cost >= base_cost,
1107                    "n={n}: forbidding solution edge {i}-{j} improved cost {base_cost} -> {new_cost}"
1108                );
1109            }
1110
1111            // Forbid a handful of edges the solution doesn't use: cost is fixed.
1112            let mut checked = 0;
1113            'outer: for i in 0..n {
1114                for j in (i + 1)..n {
1115                    if base_mate[i] == j {
1116                        continue;
1117                    }
1118                    let mut c2 = cost.clone();
1119                    c2[i][j] = PENALTY;
1120                    c2[j][i] = PENALTY;
1121                    let m2 = min_weight_perfect_matching(&flat(&c2), c2.len());
1122                    assert_ne!(m2[i], j, "n={n}: forbidden unused edge {i}-{j} was used");
1123                    let new_cost = total_of(&cost, &m2);
1124                    assert_eq!(
1125                        new_cost, base_cost,
1126                        "n={n}: forbidding unused edge {i}-{j} changed optimum {base_cost} -> {new_cost}"
1127                    );
1128                    checked += 1;
1129                    if checked >= 5 {
1130                        break 'outer;
1131                    }
1132                    break; // spread the sample across distinct vertices
1133                }
1134            }
1135        }
1136    }
1137}