use crate::graph::core::Graph;
use crate::linalg::matrix::Matrix;
#[must_use]
pub fn hopcroft_karp(left_n: usize, right_n: usize, edges: &[(usize, usize)]) -> Vec<Option<usize>> {
let mut adj = vec![Vec::new(); left_n];
for &(l, r) in edges {
assert!(l < left_n, "left vertex {l} is outside 0..{left_n}");
assert!(r < right_n, "right vertex {r} is outside 0..{right_n}");
adj[l].push(r);
}
let mut match_l: Vec<Option<usize>> = vec![None; left_n];
let mut match_r: Vec<Option<usize>> = vec![None; right_n];
loop {
let mut dist = vec![usize::MAX; left_n];
let mut queue = std::collections::VecDeque::new();
for l in 0..left_n {
if match_l[l].is_none() {
dist[l] = 0;
queue.push_back(l);
}
}
let mut found = false;
while let Some(l) = queue.pop_front() {
for &r in &adj[l] {
match match_r[r] {
None => found = true,
Some(next) if dist[next] == usize::MAX => {
dist[next] = dist[l] + 1;
queue.push_back(next);
}
Some(_) => {}
}
}
}
if !found {
break;
}
for l in 0..left_n {
if match_l[l].is_none() {
hk_augment(l, &adj, &mut match_l, &mut match_r, &mut dist);
}
}
}
match_l
}
fn hk_augment(
l: usize,
adj: &[Vec<usize>],
match_l: &mut [Option<usize>],
match_r: &mut [Option<usize>],
dist: &mut [usize],
) -> bool {
for idx in 0..adj[l].len() {
let r = adj[l][idx];
let ok = match match_r[r] {
None => true,
Some(next) => dist[next] == dist[l] + 1 && hk_augment(next, adj, match_l, match_r, dist),
};
if ok {
match_l[l] = Some(r);
match_r[r] = Some(l);
return true;
}
}
dist[l] = usize::MAX;
false
}
#[must_use]
pub fn hungarian(cost: &Matrix) -> (f64, Vec<usize>) {
assert_eq!(cost.rows, cost.cols, "the cost matrix must be square");
assert!(
cost.data.iter().all(|x| x.is_finite()),
"costs must be finite"
);
let n = cost.rows;
let mut u = vec![0.0f64; n + 1];
let mut v = vec![0.0f64; n + 1];
let mut p = vec![0usize; n + 1];
let mut way = vec![0usize; n + 1];
for i in 1..=n {
p[0] = i;
let mut j0 = 0usize;
let mut min_v = vec![f64::INFINITY; n + 1];
let mut used = vec![false; n + 1];
loop {
used[j0] = true;
let i0 = p[j0];
let mut delta = f64::INFINITY;
let mut j1 = 0usize;
for j in 1..=n {
if used[j] {
continue;
}
let cur = cost.get(i0 - 1, j - 1) - u[i0] - v[j];
if cur < min_v[j] {
min_v[j] = cur;
way[j] = j0;
}
if min_v[j] < delta {
delta = min_v[j];
j1 = j;
}
}
for j in 0..=n {
if used[j] {
u[p[j]] += delta;
v[j] -= delta;
} else {
min_v[j] -= delta;
}
}
j0 = j1;
if p[j0] == 0 {
break;
}
}
while j0 != 0 {
let j1 = way[j0];
p[j0] = p[j1];
j0 = j1;
}
}
let mut assignment = vec![0usize; n];
for j in 1..=n {
if p[j] != 0 {
assignment[p[j] - 1] = j - 1;
}
}
let total = (0..n).map(|i| cost.get(i, assignment[i])).sum();
(total, assignment)
}
#[must_use]
pub fn auction_assignment(cost: &Matrix, eps: f64) -> (f64, Vec<usize>) {
assert_eq!(cost.rows, cost.cols, "the cost matrix must be square");
assert!(eps > 0.0, "eps must be positive");
assert!(
cost.data.iter().all(|x| x.is_finite()),
"costs must be finite"
);
let n = cost.rows;
let value = |i: usize, j: usize| -cost.get(i, j);
let mut price = vec![0.0f64; n];
let mut owner: Vec<Option<usize>> = vec![None; n];
let mut assignment: Vec<Option<usize>> = vec![None; n];
let mut e = (n as f64).max(1.0);
while e >= eps {
owner.iter_mut().for_each(|o| *o = None);
assignment.iter_mut().for_each(|a| *a = None);
let mut guard = 0usize;
let cap = 1_000 * n * n + 1_000;
while assignment.iter().any(Option::is_none) && guard < cap {
guard += 1;
let i = assignment.iter().position(Option::is_none).unwrap();
let mut best_j = 0usize;
let mut best = f64::NEG_INFINITY;
let mut second = f64::NEG_INFINITY;
for j in 0..n {
let net = value(i, j) - price[j];
if net > best {
second = best;
best = net;
best_j = j;
} else if net > second {
second = net;
}
}
price[best_j] += best - second + e;
if let Some(prev) = owner[best_j] {
assignment[prev] = None;
}
owner[best_j] = Some(i);
assignment[i] = Some(best_j);
}
e /= 4.0;
}
let out: Vec<usize> = assignment.into_iter().map(|a| a.unwrap_or(0)).collect();
let total = (0..n).map(|i| cost.get(i, out[i])).sum();
(total, out)
}
#[must_use]
pub fn blossom_max_matching(g: &Graph) -> Vec<Option<usize>> {
assert!(!g.directed, "a matching is defined on an undirected graph");
let n = g.n;
let mut adj = vec![Vec::new(); n];
for (u, v, _) in g.edges() {
if u != v {
adj[u].push(v);
adj[v].push(u);
}
}
const NONE: usize = usize::MAX;
let mut mate = vec![NONE; n];
for u in 0..n {
if mate[u] == NONE {
if let Some(&v) = adj[u].iter().find(|&&v| mate[v] == NONE) {
mate[u] = v;
mate[v] = u;
}
}
}
let mut parent = vec![NONE; n];
let mut base: Vec<usize> = (0..n).collect();
let mut outer = vec![false; n];
for root in 0..n {
if mate[root] != NONE {
continue;
}
outer.iter_mut().for_each(|x| *x = false);
parent.iter_mut().for_each(|x| *x = NONE);
for (i, b) in base.iter_mut().enumerate() {
*b = i;
}
outer[root] = true;
let mut queue = std::collections::VecDeque::from(vec![root]);
let mut found = NONE;
'search: while let Some(v) = queue.pop_front() {
for idx in 0..adj[v].len() {
let to = adj[v][idx];
if base[v] == base[to] || mate[v] == to {
continue;
}
if to == root || (mate[to] != NONE && parent[mate[to]] != NONE) {
let curbase = blossom_base(&base, &mate, &parent, v, to);
let mut in_blossom = vec![false; n];
mark_blossom_path(&base, &mate, &mut parent, &mut in_blossom, v, curbase, to);
mark_blossom_path(&base, &mate, &mut parent, &mut in_blossom, to, curbase, v);
for i in 0..n {
if in_blossom[base[i]] {
base[i] = curbase;
if !outer[i] {
outer[i] = true;
queue.push_back(i);
}
}
}
} else if parent[to] == NONE {
parent[to] = v;
if mate[to] == NONE {
found = to;
break 'search;
}
outer[mate[to]] = true;
queue.push_back(mate[to]);
}
}
}
let mut u = found;
while u != NONE {
let pv = parent[u];
let ppv = mate[pv];
mate[u] = pv;
mate[pv] = u;
u = ppv;
}
}
mate
.into_iter()
.map(|x| if x == NONE { None } else { Some(x) })
.collect()
}
fn blossom_base(
base: &[usize],
mate: &[usize],
parent: &[usize],
mut u: usize,
mut v: usize,
) -> usize {
const NONE: usize = usize::MAX;
let mut seen = vec![false; base.len()];
loop {
u = base[u];
seen[u] = true;
if mate[u] == NONE {
break;
}
u = parent[mate[u]];
}
loop {
v = base[v];
if seen[v] {
return v;
}
v = parent[mate[v]];
}
}
fn mark_blossom_path(
base: &[usize],
mate: &[usize],
parent: &mut [usize],
in_blossom: &mut [bool],
mut v: usize,
b: usize,
mut child: usize,
) {
while base[v] != b {
in_blossom[base[v]] = true;
in_blossom[base[mate[v]]] = true;
parent[v] = child;
child = mate[v];
v = parent[mate[v]];
}
}
#[must_use]
pub fn stable_marriage(prefs_a: &[Vec<usize>], prefs_b: &[Vec<usize>]) -> Vec<usize> {
let n = prefs_a.len();
assert_eq!(prefs_b.len(), n, "both sides must be the same size");
for p in prefs_a.iter().chain(prefs_b.iter()) {
assert!(
crate::discrete::combinatorics::is_permutation(p) && p.len() == n,
"each preference list must rank every member of the other side"
);
}
let mut rank_b = vec![vec![0usize; n]; n];
for (j, p) in prefs_b.iter().enumerate() {
for (r, &i) in p.iter().enumerate() {
rank_b[j][i] = r;
}
}
let mut next_proposal = vec![0usize; n];
let mut partner_b: Vec<Option<usize>> = vec![None; n];
let mut free: Vec<usize> = (0..n).rev().collect();
while let Some(i) = free.pop() {
let j = prefs_a[i][next_proposal[i]];
next_proposal[i] += 1;
match partner_b[j] {
None => partner_b[j] = Some(i),
Some(k) if rank_b[j][i] < rank_b[j][k] => {
partner_b[j] = Some(i);
free.push(k);
}
Some(_) => free.push(i),
}
}
let mut out = vec![0usize; n];
for (j, p) in partner_b.iter().enumerate() {
out[p.expect("every receiver ends matched")] = j;
}
out
}
#[must_use]
pub fn stable_roommates(prefs: &[Vec<usize>]) -> Option<Vec<usize>> {
let n = prefs.len();
assert!(n.is_multiple_of(2), "a roommates instance needs an even size");
for (i, p) in prefs.iter().enumerate() {
assert_eq!(p.len(), n - 1, "each list must rank the other {} people", n - 1);
let mut sorted = p.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), n - 1, "person {i} has a repeated preference");
assert!(!p.contains(&i), "person {i} cannot rank themselves");
}
let mut rank = vec![vec![usize::MAX; n]; n];
for (i, p) in prefs.iter().enumerate() {
for (r, &j) in p.iter().enumerate() {
rank[i][j] = r;
}
}
let mut list: Vec<Vec<usize>> = prefs.to_vec();
let mut held: Vec<Option<usize>> = vec![None; n];
let mut next = vec![0usize; n];
let mut free: Vec<usize> = (0..n).rev().collect();
while let Some(i) = free.pop() {
loop {
if next[i] >= list[i].len() {
return None;
}
let j = list[i][next[i]];
next[i] += 1;
match held[j] {
None => {
held[j] = Some(i);
break;
}
Some(k) if rank[j][i] < rank[j][k] => {
held[j] = Some(i);
free.push(k);
break;
}
Some(_) => {}
}
}
}
if held.iter().any(Option::is_none) {
return None;
}
for j in 0..n {
let h = held[j].unwrap();
let cutoff = rank[j][h];
list[j].retain(|&x| rank[j][x] <= cutoff);
}
for i in 0..n {
let keep: Vec<usize> = list[i]
.iter()
.copied()
.filter(|&j| list[j].contains(&i))
.collect();
list[i] = keep;
}
loop {
if list.iter().any(Vec::is_empty) {
return None;
}
let Some(start) = (0..n).find(|&i| list[i].len() > 1) else {
break;
};
let mut xs = Vec::new();
let mut ys = Vec::new();
let mut seen = vec![usize::MAX; n];
let mut p = start;
let mut step = 0usize;
loop {
if seen[p] != usize::MAX {
let cut = seen[p];
xs.drain(..cut);
ys.drain(..cut);
break;
}
seen[p] = step;
step += 1;
if list[p].len() < 2 {
return None;
}
let q = list[p][1];
xs.push(p);
ys.push(q);
p = *list[q].last().expect("the list is non-empty");
}
for k in 0..xs.len() {
let y = ys[k];
let x_next = xs[(k + 1) % xs.len()];
let cutoff = rank[y][x_next];
let doomed: Vec<usize> = list[y]
.iter()
.copied()
.filter(|&z| rank[y][z] >= cutoff)
.collect();
list[y].retain(|&z| rank[y][z] < cutoff);
for z in doomed {
list[z].retain(|&w| w != y);
}
}
}
let out: Vec<usize> = (0..n).map(|i| list[i][0]).collect();
if (0..n).any(|i| out[out[i]] != i) {
return None;
}
Some(out)
}
#[must_use]
pub fn konig_vertex_cover(g: &Graph, left: &[usize], matching: &[Option<usize>]) -> Vec<usize> {
let n = g.n;
let mut is_left = vec![false; n];
for &v in left {
assert!(v < n, "vertex {v} is outside 0..{n}");
assert!(!is_left[v], "vertex {v} appears twice");
is_left[v] = true;
}
for v in 0..n {
if let Some(w) = matching[v] {
assert_eq!(matching[w], Some(v), "the matching is not symmetric");
}
}
let mut adj = vec![Vec::new(); n];
for (u, v, _) in g.edges() {
if u != v {
adj[u].push(v);
adj[v].push(u);
}
}
let mut seen = vec![false; n];
let mut stack: Vec<usize> = (0..n)
.filter(|&v| is_left[v] && matching[v].is_none())
.collect();
for &v in &stack {
seen[v] = true;
}
while let Some(v) = stack.pop() {
if is_left[v] {
for &w in &adj[v] {
if matching[v] != Some(w) && !seen[w] {
seen[w] = true;
stack.push(w);
}
}
} else if let Some(w) = matching[v] {
if !seen[w] {
seen[w] = true;
stack.push(w);
}
}
}
(0..n)
.filter(|&v| if is_left[v] { !seen[v] } else { seen[v] })
.collect()
}
pub fn hall_condition_check(g: &Graph, left: &[usize]) -> Result<(), Vec<usize>> {
let n = g.n;
let mut is_left = vec![false; n];
for &v in left {
is_left[v] = true;
}
let right: Vec<usize> = (0..n).filter(|&v| !is_left[v]).collect();
let mut index_l = vec![usize::MAX; n];
let mut index_r = vec![usize::MAX; n];
for (i, &v) in left.iter().enumerate() {
index_l[v] = i;
}
for (i, &v) in right.iter().enumerate() {
index_r[v] = i;
}
let mut edges = Vec::new();
for (u, v, _) in g.edges() {
if u == v {
continue;
}
let (a, b) = if is_left[u] { (u, v) } else { (v, u) };
if index_l[a] != usize::MAX && index_r[b] != usize::MAX {
edges.push((index_l[a], index_r[b]));
}
}
let m = hopcroft_karp(left.len(), right.len(), &edges);
if m.iter().all(Option::is_some) {
return Ok(());
}
let mut adj_l = vec![Vec::new(); left.len()];
let mut match_r: Vec<Option<usize>> = vec![None; right.len()];
for &(a, b) in &edges {
adj_l[a].push(b);
}
for (a, &b) in m.iter().enumerate() {
if let Some(b) = b {
match_r[b] = Some(a);
}
}
let mut seen_l = vec![false; left.len()];
let mut seen_r = vec![false; right.len()];
let mut stack: Vec<usize> = (0..left.len()).filter(|&a| m[a].is_none()).collect();
for &a in &stack {
seen_l[a] = true;
}
while let Some(a) = stack.pop() {
for &b in &adj_l[a] {
if !seen_r[b] {
seen_r[b] = true;
if let Some(c) = match_r[b] {
if !seen_l[c] {
seen_l[c] = true;
stack.push(c);
}
}
}
}
}
Err((0..left.len())
.filter(|&a| seen_l[a])
.map(|a| left[a])
.collect())
}
#[must_use]
pub fn maximum_weight_bipartite(weights: &Matrix) -> (f64, Vec<Option<usize>>) {
let (rows, cols) = (weights.rows, weights.cols);
let n = rows.max(cols);
let mut cost = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
let w = if i < rows && j < cols {
weights.get(i, j).max(0.0)
} else {
0.0
};
cost.set(i, j, -w);
}
}
let (_, assignment) = hungarian(&cost);
let mut partner = vec![None; rows];
let mut total = 0.0;
for i in 0..rows {
let j = assignment[i];
if j < cols && weights.get(i, j) > 0.0 {
partner[i] = Some(j);
total += weights.get(i, j);
}
}
(total, partner)
}
#[must_use]
pub fn matching_size(m: &[Option<usize>]) -> usize {
m.iter().filter(|x| x.is_some()).count() / 2
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discrete::combinatorics::{combinations_iter, permutations_iter};
use crate::graph::core::{complete_bipartite, complete_graph, cycle_graph, petersen_graph};
use crate::graph::flow::max_bipartite_matching_via_flow;
use crate::monte_carlo::Rng;
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6 * a.abs().max(b.abs()).max(1.0)
}
fn random_bipartite(l: usize, r: usize, p: f64, rng: &mut Rng) -> (Vec<(usize, usize)>, Graph) {
let mut edges = Vec::new();
let mut g = Graph::new(l + r, false);
for a in 0..l {
for b in 0..r {
if rng.next_f64() < p {
edges.push((a, b));
g.add_edge(a, l + b, 1.0);
}
}
}
(edges, g)
}
fn brute_bipartite(l: usize, edges: &[(usize, usize)]) -> usize {
let mut best = 0usize;
for k in (1..=edges.len()).rev() {
if k <= best {
break;
}
for combo in combinations_iter(edges.len(), k) {
let mut used_l = vec![false; l];
let mut used_r = std::collections::BTreeSet::new();
let mut ok = true;
for &i in &combo {
let (a, b) = edges[i];
if used_l[a] || !used_r.insert(b) {
ok = false;
break;
}
used_l[a] = true;
}
if ok {
best = k;
break;
}
}
if best == k {
break;
}
}
best
}
#[test]
fn hopcroft_karp_is_maximum() {
let mut rng = Rng::new(0x_4C41);
for l in 1..=5usize {
for r in 1..=5usize {
for _ in 0..20 {
let (edges, g) = random_bipartite(l, r, 0.45, &mut rng);
let m = hopcroft_karp(l, r, &edges);
let mut seen_r = std::collections::BTreeSet::new();
for (a, &b) in m.iter().enumerate() {
if let Some(b) = b {
assert!(edges.contains(&(a, b)), "matched a non-edge");
assert!(seen_r.insert(b), "right vertex {b} matched twice");
}
}
let size = m.iter().filter(|x| x.is_some()).count();
assert_eq!(size, brute_bipartite(l, &edges), "l = {l}, r = {r}");
let left: Vec<usize> = (0..l).collect();
let via_flow = max_bipartite_matching_via_flow(&g, &left);
let flow_size = via_flow.iter().filter(|x| x.is_some()).count() / 2;
assert_eq!(size, flow_size, "Hopcroft-Karp vs flow");
}
}
}
for l in 1..=5usize {
for r in 1..=5usize {
let edges: Vec<(usize, usize)> =
(0..l).flat_map(|a| (0..r).map(move |b| (a, b))).collect();
let m = hopcroft_karp(l, r, &edges);
assert_eq!(m.iter().filter(|x| x.is_some()).count(), l.min(r));
}
}
assert_eq!(hopcroft_karp(3, 3, &[]), vec![None, None, None]);
}
#[test]
fn konig_cover_matches_the_matching_size() {
let mut rng = Rng::new(0x_C061);
for l in 1..=5usize {
for r in 1..=5usize {
for _ in 0..15 {
let (edges, g) = random_bipartite(l, r, 0.45, &mut rng);
let left: Vec<usize> = (0..l).collect();
let m = max_bipartite_matching_via_flow(&g, &left);
let size = matching_size(&m);
let cover = konig_vertex_cover(&g, &left, &m);
assert_eq!(cover.len(), size, "Konig: cover {} vs matching {size}", cover.len());
for (u, v, _) in g.edges() {
assert!(
cover.contains(&u) || cover.contains(&v),
"edge ({u}, {v}) is uncovered"
);
}
let n = l + r;
let mut best = n;
for k in 0..=n {
let mut found = false;
for combo in combinations_iter(n, k) {
if g.edges()
.iter()
.all(|&(u, v, _)| combo.contains(&u) || combo.contains(&v))
{
found = true;
break;
}
}
if found {
best = k;
break;
}
}
assert_eq!(cover.len(), best, "cover is not minimum");
let _ = edges;
}
}
}
}
#[test]
fn hall_condition_matches_subset_search() {
let mut rng = Rng::new(0x_4A11);
for l in 1..=5usize {
for r in 1..=5usize {
for _ in 0..15 {
let (_, g) = random_bipartite(l, r, 0.4, &mut rng);
let left: Vec<usize> = (0..l).collect();
let mut violator: Option<Vec<usize>> = None;
for k in 1..=l {
for combo in combinations_iter(l, k) {
let mut nbrs = std::collections::BTreeSet::new();
for &a in &combo {
for &(w, _) in &g.adj[a] {
nbrs.insert(w);
}
}
if nbrs.len() < combo.len() {
violator = Some(combo.clone());
break;
}
}
if violator.is_some() {
break;
}
}
match hall_condition_check(&g, &left) {
Ok(()) => {
assert!(violator.is_none(), "condition passed but {violator:?} violates");
let m = max_bipartite_matching_via_flow(&g, &left);
assert_eq!(matching_size(&m), l, "Hall holds but no saturation");
}
Err(set) => {
assert!(violator.is_some(), "condition failed but none violates");
let mut nbrs = std::collections::BTreeSet::new();
for &a in &set {
for &(w, _) in &g.adj[a] {
nbrs.insert(w);
}
}
assert!(
nbrs.len() < set.len(),
"reported set {set:?} has {} neighbours, not fewer than {}",
nbrs.len(),
set.len()
);
}
}
}
}
}
}
#[test]
fn hungarian_matches_brute_force() {
let mut rng = Rng::new(0x_4055);
for n in 1..=7usize {
for _ in 0..15 {
let mut cost = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
cost.set(i, j, (20.0 * rng.next_f64() - 5.0).round());
}
}
let (total, assign) = hungarian(&cost);
assert!(
crate::discrete::combinatorics::is_permutation(&assign),
"the assignment is not a permutation: {assign:?}"
);
let actual: f64 = (0..n).map(|i| cost.get(i, assign[i])).sum();
assert!(close(total, actual), "reported {total} but costs {actual}");
let best = permutations_iter(&(0..n).collect::<Vec<_>>())
.map(|p| (0..n).map(|i| cost.get(i, p[i])).sum::<f64>())
.fold(f64::INFINITY, f64::min);
assert!(close(total, best), "n = {n}: {total} vs brute {best}");
}
}
let mut c = Matrix::zeros(4, 4);
for i in 0..4 {
for j in 0..4 {
c.set(i, j, if i == j { 0.0 } else { 1.0 });
}
}
let (t, a) = hungarian(&c);
assert!(close(t, 0.0));
assert_eq!(a, vec![0, 1, 2, 3]);
let one = Matrix { rows: 1, cols: 1, data: vec![7.0] };
assert_eq!(hungarian(&one), (7.0, vec![0]));
}
#[test]
fn auction_reaches_the_hungarian_optimum() {
let mut rng = Rng::new(0x_A0C7);
for n in 1..=6usize {
for _ in 0..12 {
let mut cost = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
cost.set(i, j, (20.0 * rng.next_f64()).round());
}
}
let (opt, _) = hungarian(&cost);
let (got, assign) = auction_assignment(&cost, 1e-3);
assert!(
crate::discrete::combinatorics::is_permutation(&assign),
"auction produced {assign:?}"
);
let actual: f64 = (0..n).map(|i| cost.get(i, assign[i])).sum();
assert!(close(got, actual), "reported {got} but costs {actual}");
assert!(
got <= opt + n as f64 * 1e-3 + 1e-9,
"n = {n}: auction {got} exceeds optimum {opt}"
);
assert!(got >= opt - 1e-9, "auction beat the optimum");
}
}
}
#[test]
fn blossom_matches_brute_force_on_general_graphs() {
let mut rng = Rng::new(0x_B105);
for n in 1..=8usize {
for _ in 0..20 {
let mut g = Graph::new(n, false);
for u in 0..n {
for v in u + 1..n {
if rng.next_f64() < 0.4 {
g.add_edge(u, v, 1.0);
}
}
}
let m = blossom_max_matching(&g);
for v in 0..n {
if let Some(w) = m[v] {
assert_eq!(m[w], Some(v), "not symmetric at {v}");
assert!(
g.adj[v].iter().any(|&(x, _)| x == w),
"matched a non-edge ({v}, {w})"
);
}
}
assert_eq!(matching_size(&m), brute_general(&g), "n = {n}");
}
}
let p = petersen_graph();
let m = blossom_max_matching(&p);
assert_eq!(matching_size(&m), 5, "Petersen has a perfect matching");
assert!(m.iter().all(Option::is_some), "every vertex must be matched");
for n in [3usize, 5, 7, 9] {
let c = cycle_graph(n);
assert_eq!(matching_size(&blossom_max_matching(&c)), n / 2, "C{n}");
}
for n in [4usize, 6, 8] {
assert_eq!(matching_size(&blossom_max_matching(&cycle_graph(n))), n / 2);
}
for n in 1..=8usize {
assert_eq!(
matching_size(&blossom_max_matching(&complete_graph(n))),
n / 2,
"K{n}"
);
}
let tri = Graph::from_edges(
4,
&[(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0), (2, 3, 1.0)],
false,
);
assert_eq!(matching_size(&blossom_max_matching(&tri)), 2);
}
fn brute_general(g: &Graph) -> usize {
let edges: Vec<(usize, usize)> = g
.edges()
.into_iter()
.filter(|&(u, v, _)| u != v)
.map(|(u, v, _)| (u, v))
.collect();
let mut best = 0usize;
for k in (1..=edges.len()).rev() {
if k <= best {
break;
}
for combo in combinations_iter(edges.len(), k) {
let mut used = vec![false; g.n];
let mut ok = true;
for &i in &combo {
let (u, v) = edges[i];
if used[u] || used[v] {
ok = false;
break;
}
used[u] = true;
used[v] = true;
}
if ok {
best = k;
break;
}
}
if best == k {
break;
}
}
best
}
#[test]
fn gale_shapley_is_stable_and_a_optimal() {
let mut rng = Rng::new(0x_6A1E);
for n in 1..=5usize {
for _ in 0..25 {
let prefs_a: Vec<Vec<usize>> = (0..n)
.map(|_| crate::discrete::combinatorics::random_permutation(n, &mut rng))
.collect();
let prefs_b: Vec<Vec<usize>> = (0..n)
.map(|_| crate::discrete::combinatorics::random_permutation(n, &mut rng))
.collect();
let m = stable_marriage(&prefs_a, &prefs_b);
assert!(
crate::discrete::combinatorics::is_permutation(&m),
"not a perfect matching: {m:?}"
);
assert!(is_stable(&prefs_a, &prefs_b, &m), "unstable: {m:?}");
for other in permutations_iter(&(0..n).collect::<Vec<_>>()) {
if !is_stable(&prefs_a, &prefs_b, &other) {
continue;
}
for i in 0..n {
let rank_got = prefs_a[i].iter().position(|&x| x == m[i]).unwrap();
let rank_other = prefs_a[i].iter().position(|&x| x == other[i]).unwrap();
assert!(
rank_got <= rank_other,
"proposer {i} could do better in {other:?}"
);
}
}
}
}
}
fn is_stable(prefs_a: &[Vec<usize>], prefs_b: &[Vec<usize>], m: &[usize]) -> bool {
let n = m.len();
let mut partner_b = vec![0usize; n];
for (i, &j) in m.iter().enumerate() {
partner_b[j] = i;
}
let rank = |p: &[usize], x: usize| p.iter().position(|&y| y == x).unwrap();
for i in 0..n {
for j in 0..n {
if j == m[i] {
continue;
}
let i_prefers = rank(&prefs_a[i], j) < rank(&prefs_a[i], m[i]);
let j_prefers = rank(&prefs_b[j], i) < rank(&prefs_b[j], partner_b[j]);
if i_prefers && j_prefers {
return false;
}
}
}
true
}
#[test]
fn stable_roommates_agrees_with_exhaustive_search() {
let mut rng = Rng::new(0x_2001);
for n in [2usize, 4, 6] {
for _ in 0..40 {
let prefs: Vec<Vec<usize>> = (0..n)
.map(|i| {
let others: Vec<usize> = (0..n).filter(|&x| x != i).collect();
let perm = crate::discrete::combinatorics::random_permutation(
others.len(),
&mut rng,
);
perm.into_iter().map(|k| others[k]).collect()
})
.collect();
let brute = brute_roommates(&prefs);
match stable_roommates(&prefs) {
Some(m) => {
assert!(m.iter().enumerate().all(|(i, &j)| m[j] == i), "not a pairing");
assert!(roommates_stable(&prefs, &m), "returned an unstable pairing");
assert!(brute.is_some(), "found one where exhaustive search found none");
}
None => assert!(
brute.is_none(),
"reported none but {brute:?} is stable"
),
}
}
}
let none = vec![
vec![1, 2, 3],
vec![2, 0, 3],
vec![0, 1, 3],
vec![0, 1, 2],
];
assert!(brute_roommates(&none).is_none(), "the instance must be unsolvable");
assert!(stable_roommates(&none).is_none());
assert_eq!(stable_roommates(&[vec![1], vec![0]]), Some(vec![1, 0]));
}
fn brute_roommates(prefs: &[Vec<usize>]) -> Option<Vec<usize>> {
let n = prefs.len();
for perm in permutations_iter(&(0..n).collect::<Vec<_>>()) {
if (0..n).any(|i| perm[perm[i]] != i || perm[i] == i) {
continue;
}
if roommates_stable(prefs, &perm) {
return Some(perm);
}
}
None
}
fn roommates_stable(prefs: &[Vec<usize>], m: &[usize]) -> bool {
let n = m.len();
let rank = |i: usize, x: usize| prefs[i].iter().position(|&y| y == x).unwrap();
for i in 0..n {
for j in 0..n {
if i == j || m[i] == j {
continue;
}
if rank(i, j) < rank(i, m[i]) && rank(j, i) < rank(j, m[j]) {
return false;
}
}
}
true
}
#[test]
fn maximum_weight_bipartite_beats_every_alternative() {
let mut rng = Rng::new(0x_1471);
for rows in 1..=5usize {
for cols in 1..=5usize {
for _ in 0..12 {
let mut w = Matrix::zeros(rows, cols);
for i in 0..rows {
for j in 0..cols {
let v = (12.0 * rng.next_f64() - 4.0).round();
w.set(i, j, v.max(0.0));
}
}
let (total, partner) = maximum_weight_bipartite(&w);
let mut seen = std::collections::BTreeSet::new();
let mut actual = 0.0;
for (i, &p) in partner.iter().enumerate() {
if let Some(j) = p {
assert!(seen.insert(j), "column {j} used twice");
assert!(w.get(i, j) > 0.0, "matched a zero-weight pair");
actual += w.get(i, j);
}
}
assert!(close(total, actual), "reported {total} but sums to {actual}");
let best = brute_weighted(&w);
assert!(close(total, best), "{rows}x{cols}: {total} vs brute {best}");
}
}
}
let (t, p) = maximum_weight_bipartite(&Matrix::zeros(3, 3));
assert!(close(t, 0.0));
assert!(p.iter().all(Option::is_none));
assert_eq!(
maximum_weight_bipartite(&Matrix { rows: 1, cols: 1, data: vec![3.0] }),
(3.0, vec![Some(0)])
);
assert_eq!(
maximum_weight_bipartite(&Matrix { rows: 1, cols: 1, data: vec![0.0] }),
(0.0, vec![None])
);
}
fn brute_weighted(w: &Matrix) -> f64 {
let (rows, cols) = (w.rows, w.cols);
let mut best = 0.0f64;
for k in 0..=rows.min(cols) {
for row_set in combinations_iter(rows, k) {
for col_set in combinations_iter(cols, k) {
for perm in permutations_iter(&(0..k).collect::<Vec<_>>()) {
let total: f64 = (0..k)
.map(|idx| w.get(row_set[idx], col_set[perm[idx]]))
.sum();
best = best.max(total);
}
}
}
}
best
}
#[test]
fn matching_size_counts_pairs() {
assert_eq!(matching_size(&[None, None]), 0);
assert_eq!(matching_size(&[Some(1), Some(0)]), 1);
assert_eq!(matching_size(&[Some(1), Some(0), Some(3), Some(2)]), 2);
assert_eq!(matching_size(&[Some(1), Some(0), None, None]), 1);
let g = complete_bipartite(3, 4);
let left: Vec<usize> = (0..3).collect();
assert_eq!(matching_size(&max_bipartite_matching_via_flow(&g, &left)), 3);
}
}