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}