use std::collections::VecDeque;
use crate::incidence::EqualityIncidence;
const NIL: usize = usize::MAX;
const INF: usize = usize::MAX;
#[derive(Debug, Clone, Default)]
pub struct BipartiteMatching {
pub row_to_var: Vec<Option<usize>>,
pub var_to_row: Vec<Option<usize>>,
pub size: usize,
}
pub fn hopcroft_karp(inc: &EqualityIncidence) -> BipartiteMatching {
let n_rows = inc.n_eq_rows();
let n_vars = inc.n_vars;
let mut pair_u: Vec<usize> = vec![NIL; n_rows];
let mut pair_v: Vec<usize> = vec![NIL; n_vars];
let mut dist: Vec<usize> = vec![INF; n_rows];
let mut size = 0;
let max_rounds = (n_rows.min(n_vars) + 1) * 2;
let mut round = 0;
while bfs(inc, &pair_u, &pair_v, &mut dist) {
let mut augmented = false;
for r in 0..n_rows {
if pair_u[r] == NIL && dfs(inc, r, &mut pair_u, &mut pair_v, &mut dist) {
size += 1;
augmented = true;
}
}
if !augmented {
break;
}
round += 1;
debug_assert!(round <= max_rounds, "Hopcroft-Karp round cap exceeded");
}
let row_to_var = pair_u
.iter()
.map(|&v| if v == NIL { None } else { Some(v) })
.collect();
let var_to_row = pair_v
.iter()
.map(|&r| if r == NIL { None } else { Some(r) })
.collect();
BipartiteMatching {
row_to_var,
var_to_row,
size,
}
}
fn bfs(inc: &EqualityIncidence, pair_u: &[usize], pair_v: &[usize], dist: &mut [usize]) -> bool {
let mut queue: VecDeque<usize> = VecDeque::new();
for r in 0..inc.n_eq_rows() {
if pair_u[r] == NIL {
dist[r] = 0;
queue.push_back(r);
} else {
dist[r] = INF;
}
}
let mut dist_nil = INF;
while let Some(r) = queue.pop_front() {
if dist[r] >= dist_nil {
continue;
}
for &v in inc.neighbors(r) {
let next = pair_v[v];
if next == NIL {
if dist_nil == INF {
dist_nil = dist[r] + 1;
}
} else if dist[next] == INF {
dist[next] = dist[r] + 1;
queue.push_back(next);
}
}
}
dist_nil != INF
}
fn dfs(
inc: &EqualityIncidence,
r: usize,
pair_u: &mut [usize],
pair_v: &mut [usize],
dist: &mut [usize],
) -> bool {
for &v in inc.neighbors(r) {
let next = pair_v[v];
let ok = if next == NIL {
true
} else if dist[next] == dist[r] + 1 {
dfs(inc, next, pair_u, pair_v, dist)
} else {
false
};
if ok {
pair_u[r] = v;
pair_v[v] = r;
return true;
}
}
dist[r] = INF;
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incidence::ProbeView;
use pounce_common::types::{Index, Number};
fn eq_inc(n_vars: usize, n_rows: usize, edges: &[(usize, usize)]) -> EqualityIncidence {
let mut per_row: Vec<Vec<usize>> = vec![Vec::new(); n_rows];
for &(r, v) in edges {
per_row[r].push(v);
}
let mut adj_ptr = Vec::with_capacity(n_rows + 1);
let mut vars = Vec::new();
adj_ptr.push(0);
for row in per_row.iter_mut() {
row.sort_unstable();
row.dedup();
vars.extend_from_slice(row);
adj_ptr.push(vars.len());
}
EqualityIncidence {
n_vars,
eq_row_inner_idx: (0..n_rows).collect(),
adj_ptr,
vars,
}
}
#[test]
fn square_full_match_3x3() {
let inc = eq_inc(3, 3, &[(0, 0), (1, 1), (2, 2)]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 3);
assert_eq!(m.row_to_var, vec![Some(0), Some(1), Some(2)]);
}
#[test]
fn under_determined_2x3() {
let inc = eq_inc(3, 2, &[(0, 0), (0, 1), (1, 1), (1, 2)]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 2);
assert!(m.row_to_var.iter().all(|v| v.is_some()));
}
#[test]
fn over_determined_3x2() {
let inc = eq_inc(2, 3, &[(0, 0), (1, 1), (2, 0), (2, 1)]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 2);
assert_eq!(m.row_to_var.iter().filter(|v| v.is_some()).count(), 2);
}
#[test]
fn disconnected_components() {
let inc = eq_inc(2, 2, &[(0, 0), (1, 1)]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 2);
assert_eq!(m.row_to_var, vec![Some(0), Some(1)]);
}
#[test]
fn augmenting_path_required() {
let inc = eq_inc(2, 2, &[(0, 0), (0, 1), (1, 0)]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 2);
}
#[test]
fn empty_graph_yields_empty_matching() {
let inc = eq_inc(0, 0, &[]);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 0);
assert!(m.row_to_var.is_empty());
assert!(m.var_to_row.is_empty());
}
#[test]
fn matches_built_via_probeview_too() {
let irow: [Index; 3] = [0, 0, 1];
let jcol: [Index; 3] = [0, 1, 0];
let g: [Number; 2] = [0.0, 0.0];
let p = ProbeView {
n_vars: 2,
m_rows: 2,
jac_irow: &irow,
jac_jcol: &jcol,
jac_values: None,
g_l: &g,
g_u: &g,
linearity: None,
one_based: false,
eq_tol: 1e-12,
excluded_vars: None,
excluded_rows: None,
};
let inc = EqualityIncidence::from_probe(&p);
let m = hopcroft_karp(&inc);
assert_eq!(m.size, 2);
}
fn brute_force_max_matching(edges: &[(usize, usize)], n_rows: usize, n_vars: usize) -> usize {
let e = edges.len();
let mut row_used = vec![false; n_rows];
let mut var_used = vec![false; n_vars];
let mut best = 0;
for mask in 0u32..(1u32 << e) {
row_used.iter_mut().for_each(|v| *v = false);
var_used.iter_mut().for_each(|v| *v = false);
let mut count = 0;
let mut valid = true;
for k in 0..e {
if (mask >> k) & 1 == 1 {
let (r, v) = edges[k];
if row_used[r] || var_used[v] {
valid = false;
break;
}
row_used[r] = true;
var_used[v] = true;
count += 1;
}
}
if valid && count > best {
best = count;
}
}
best
}
#[test]
fn konigs_theorem_random_check() {
let mut state: u64 = 0xdead_beef_cafe_f00d;
let mut next = || -> u64 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
state >> 32
};
for _ in 0..20 {
let n_rows = 1 + (next() % 4) as usize; let n_vars = 1 + (next() % 4) as usize; let max_edges = (n_rows * n_vars).min(8); let n_edges = (next() % (max_edges as u64 + 1)) as usize;
let mut edges_set = std::collections::BTreeSet::<(usize, usize)>::new();
let mut draws = 0usize;
while edges_set.len() < n_edges {
let r = (next() % n_rows as u64) as usize;
let v = (next() % n_vars as u64) as usize;
edges_set.insert((r, v));
draws += 1;
assert!(draws < 10_000, "edge draw loop is not making progress");
}
let edges: Vec<(usize, usize)> = edges_set.into_iter().collect();
let inc = eq_inc(n_vars, n_rows, &edges);
let hk = hopcroft_karp(&inc).size;
let bf = brute_force_max_matching(&edges, n_rows, n_vars);
assert_eq!(
hk, bf,
"Hopcroft-Karp disagrees with brute force on {edges:?} ({n_rows}x{n_vars})"
);
}
}
}