#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MatchOutcome {
Feasible(Vec<usize>),
Infeasible(HallWitness),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HallWitness {
pub items: Vec<usize>,
pub slots: Vec<usize>,
}
pub fn assign_or_hall(adj: &[Vec<usize>], num_slots: usize) -> MatchOutcome {
let n = adj.len();
if n > num_slots {
return MatchOutcome::Infeasible(HallWitness {
items: (0..n).collect(),
slots: (0..num_slots).collect(),
});
}
let mut slot_match: Vec<Option<usize>> = vec![None; num_slots]; let mut item_match: Vec<Option<usize>> = vec![None; n]; hopcroft_karp(adj, &mut slot_match, &mut item_match);
if item_match.iter().all(|m| m.is_some()) {
return MatchOutcome::Feasible(item_match.into_iter().map(|m| m.unwrap()).collect());
}
MatchOutcome::Infeasible(extract_hall(adj, num_slots, &slot_match, &item_match))
}
fn hopcroft_karp(
adj: &[Vec<usize>],
slot_match: &mut [Option<usize>],
item_match: &mut [Option<usize>],
) {
let n = adj.len();
let num_slots = slot_match.len();
const INF: usize = usize::MAX;
loop {
let mut dist = vec![INF; n];
let mut queue = std::collections::VecDeque::new();
for u in 0..n {
if item_match[u].is_none() {
dist[u] = 0;
queue.push_back(u);
}
}
let mut reachable_free = false;
while let Some(u) = queue.pop_front() {
for &v in &adj[u] {
if v >= num_slots {
continue;
}
match slot_match[v] {
None => reachable_free = true,
Some(w) if dist[w] == INF => {
dist[w] = dist[u] + 1;
queue.push_back(w);
}
_ => {}
}
}
}
if !reachable_free {
break; }
for u in 0..n {
if item_match[u].is_none() {
hk_dfs(u, adj, slot_match, item_match, &mut dist);
}
}
}
}
fn hk_dfs(
u: usize,
adj: &[Vec<usize>],
slot_match: &mut [Option<usize>],
item_match: &mut [Option<usize>],
dist: &mut [usize],
) -> bool {
let num_slots = slot_match.len();
for idx in 0..adj[u].len() {
let v = adj[u][idx];
if v >= num_slots {
continue;
}
let proceed = match slot_match[v] {
None => true,
Some(w) => dist[w] == dist[u] + 1 && hk_dfs(w, adj, slot_match, item_match, dist),
};
if proceed {
slot_match[v] = Some(u);
item_match[u] = Some(v);
return true;
}
}
dist[u] = usize::MAX;
false
}
fn extract_hall(
adj: &[Vec<usize>],
num_slots: usize,
slot_match: &[Option<usize>],
item_match: &[Option<usize>],
) -> HallWitness {
let n = adj.len();
let mut item_reach = vec![false; n];
let mut slot_reach = vec![false; num_slots];
let mut stack: Vec<usize> = Vec::new();
for (u, m) in item_match.iter().enumerate() {
if m.is_none() {
item_reach[u] = true;
stack.push(u);
}
}
while let Some(u) = stack.pop() {
for &v in &adj[u] {
if v < num_slots && !slot_reach[v] {
slot_reach[v] = true;
if let Some(w) = slot_match[v] {
if !item_reach[w] {
item_reach[w] = true;
stack.push(w);
}
}
}
}
}
HallWitness {
items: (0..n).filter(|&u| item_reach[u]).collect(),
slots: (0..num_slots).filter(|&v| slot_reach[v]).collect(),
}
}
pub fn is_hall_witness(adj: &[Vec<usize>], w: &HallWitness) -> bool {
if w.items.len() <= w.slots.len() {
return false;
}
let slot_set: std::collections::HashSet<usize> = w.slots.iter().copied().collect();
w.items.iter().all(|&i| {
i < adj.len() && adj[i].iter().all(|s| slot_set.contains(s))
})
}
pub fn is_valid_assignment(adj: &[Vec<usize>], num_slots: usize, assignment: &[usize]) -> bool {
if assignment.len() != adj.len() {
return false;
}
let mut used = vec![false; num_slots];
assignment.iter().enumerate().all(|(i, &s)| {
let ok = s < num_slots && adj[i].contains(&s) && !used[s];
if s < num_slots {
used[s] = true;
}
ok
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CapMatchOutcome {
Feasible(Vec<usize>),
Infeasible(CapHallWitness),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CapHallWitness {
pub items: Vec<usize>,
pub slots: Vec<usize>,
}
pub fn assign_or_hall_capacitated(adj: &[Vec<usize>], capacities: &[usize]) -> CapMatchOutcome {
let num_slots = capacities.len();
let mut offset = vec![0usize; num_slots + 1];
for s in 0..num_slots {
offset[s + 1] = offset[s] + capacities[s];
}
let total = offset[num_slots];
let exp_adj: Vec<Vec<usize>> = adj
.iter()
.map(|slots| {
slots
.iter()
.filter(|&&s| s < num_slots)
.flat_map(|&s| offset[s]..offset[s + 1])
.collect()
})
.collect();
let copy_to_slot = |c: usize| offset.partition_point(|&o| o <= c) - 1;
match assign_or_hall(&exp_adj, total) {
MatchOutcome::Feasible(copy_assign) => {
CapMatchOutcome::Feasible(copy_assign.into_iter().map(copy_to_slot).collect())
}
MatchOutcome::Infeasible(w) => {
let mut slots: Vec<usize> = w.slots.into_iter().map(copy_to_slot).collect();
slots.sort_unstable();
slots.dedup();
CapMatchOutcome::Infeasible(CapHallWitness { items: w.items, slots })
}
}
}
pub fn is_cap_hall_witness(adj: &[Vec<usize>], capacities: &[usize], w: &CapHallWitness) -> bool {
let cap: usize = w.slots.iter().map(|&s| capacities.get(s).copied().unwrap_or(0)).sum();
if w.items.len() <= cap {
return false;
}
let slot_set: std::collections::HashSet<usize> = w.slots.iter().copied().collect();
w.items
.iter()
.all(|&i| i < adj.len() && adj[i].iter().all(|s| slot_set.contains(s)))
}
#[cfg(test)]
mod tests {
use super::*;
fn php(n: usize) -> (Vec<Vec<usize>>, usize) {
let holes = n - 1;
((0..n).map(|_| (0..holes).collect()).collect(), holes)
}
#[test]
fn pigeonhole_is_infeasible_with_a_genuine_hall_witness() {
for n in 2..=12 {
let (adj, slots) = php(n);
match assign_or_hall(&adj, slots) {
MatchOutcome::Infeasible(w) => {
assert!(is_hall_witness(&adj, &w), "PHP({n}) witness invalid: {w:?}");
assert_eq!(w.items.len(), n, "all {n} pigeons are deficient");
assert_eq!(w.slots.len(), n - 1, "against {} holes", n - 1);
}
other => panic!("PHP({n}) must be infeasible, got {other:?}"),
}
}
}
#[test]
fn equal_items_and_slots_is_feasible() {
for n in 1..=10 {
let adj: Vec<Vec<usize>> = (0..n).map(|_| (0..n).collect()).collect();
match assign_or_hall(&adj, n) {
MatchOutcome::Feasible(a) => {
assert!(is_valid_assignment(&adj, n, &a), "invalid assignment {a:?}");
}
other => panic!("n={n} square should be feasible, got {other:?}"),
}
}
}
#[test]
fn restricted_subset_triggers_hall() {
let adj = vec![vec![0, 1], vec![0, 1], vec![0, 1], vec![2, 3]];
match assign_or_hall(&adj, 4) {
MatchOutcome::Infeasible(w) => {
assert!(is_hall_witness(&adj, &w), "witness invalid: {w:?}");
assert!(w.items.len() > w.slots.len());
}
other => panic!("expected Hall violation, got {other:?}"),
}
}
#[test]
fn a_solvable_restricted_matching_is_feasible() {
let adj = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 0]];
match assign_or_hall(&adj, 4) {
MatchOutcome::Feasible(a) => assert!(is_valid_assignment(&adj, 4, &a)),
other => panic!("expected a feasible matching, got {other:?}"),
}
}
#[test]
fn empty_is_trivially_feasible() {
assert_eq!(assign_or_hall(&[], 0), MatchOutcome::Feasible(vec![]));
}
#[test]
fn a_bad_hall_witness_is_rejected() {
let adj = vec![vec![0, 1], vec![0, 1]];
assert!(!is_hall_witness(&adj, &HallWitness { items: vec![0], slots: vec![0, 1] }));
assert!(!is_hall_witness(&adj, &HallWitness { items: vec![0, 1], slots: vec![0] }),
"item 1 reaches slot 1 ∉ T, so {{0}} cannot cover N(S)");
}
#[test]
fn capacity_makes_overloaded_slots_infeasible() {
let adj = vec![vec![0], vec![0], vec![0], vec![0], vec![0]];
assert!(matches!(assign_or_hall_capacitated(&adj, &[5]), CapMatchOutcome::Feasible(_)));
match assign_or_hall_capacitated(&adj, &[4]) {
CapMatchOutcome::Infeasible(w) => {
assert!(is_cap_hall_witness(&adj, &[4], &w), "cap witness invalid: {w:?}");
assert_eq!(w.items.len(), 5);
assert_eq!(w.slots, vec![0]);
}
o => panic!("capacity 4 must be infeasible: {o:?}"),
}
}
#[test]
fn capacitated_assignment_respects_capacities() {
let adj: Vec<Vec<usize>> = (0..4).map(|_| vec![0, 1]).collect();
match assign_or_hall_capacitated(&adj, &[2, 2]) {
CapMatchOutcome::Feasible(a) => {
assert_eq!(a.len(), 4);
assert!(a.iter().filter(|&&s| s == 0).count() <= 2);
assert!(a.iter().filter(|&&s| s == 1).count() <= 2);
assert!(a.iter().all(|&s| s == 0 || s == 1));
}
o => panic!("should be feasible: {o:?}"),
}
let adj5: Vec<Vec<usize>> = (0..5).map(|_| vec![0, 1]).collect();
match assign_or_hall_capacitated(&adj5, &[2, 2]) {
CapMatchOutcome::Infeasible(w) => assert!(is_cap_hall_witness(&adj5, &[2, 2], &w)),
o => panic!("5 items / capacity 4 must be infeasible: {o:?}"),
}
}
#[test]
fn hopcroft_karp_finds_the_maximum_matching() {
fn kuhn_size(adj: &[Vec<usize>], num_slots: usize) -> usize {
fn aug(u: usize, adj: &[Vec<usize>], sm: &mut [Option<usize>], seen: &mut [bool]) -> bool {
for &v in &adj[u] {
if v >= seen.len() || seen[v] {
continue;
}
seen[v] = true;
if sm[v].is_none() || aug(sm[v].unwrap(), adj, sm, seen) {
sm[v] = Some(u);
return true;
}
}
false
}
let mut sm = vec![None; num_slots];
let mut count = 0;
for u in 0..adj.len() {
let mut seen = vec![false; num_slots];
if aug(u, adj, &mut sm, &mut seen) {
count += 1;
}
}
count
}
let mut s: u64 = 0x9E3779B97F4A7C15;
let mut next = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s
};
for _ in 0..300 {
let n = (next() % 9) as usize + 1;
let num_slots = (next() % 9) as usize + 1;
let adj: Vec<Vec<usize>> = (0..n)
.map(|_| (0..num_slots).filter(|_| next() % 2 == 0).collect())
.collect();
let kuhn = kuhn_size(&adj, num_slots);
let outcome = assign_or_hall(&adj, num_slots);
let feasible = matches!(outcome, MatchOutcome::Feasible(_));
assert_eq!(
feasible,
kuhn == n,
"HK/Kuhn disagree: adj={adj:?} slots={num_slots} hk_feasible={feasible} kuhn={kuhn} n={n}"
);
match outcome {
MatchOutcome::Feasible(a) => assert!(is_valid_assignment(&adj, num_slots, &a)),
MatchOutcome::Infeasible(w) => assert!(is_hall_witness(&adj, &w)),
}
}
}
}