1#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum MatchOutcome {
21 Feasible(Vec<usize>),
23 Infeasible(HallWitness),
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct HallWitness {
32 pub items: Vec<usize>,
34 pub slots: Vec<usize>,
36}
37
38pub fn assign_or_hall(adj: &[Vec<usize>], num_slots: usize) -> MatchOutcome {
44 let n = adj.len();
45 if n > num_slots {
50 return MatchOutcome::Infeasible(HallWitness {
51 items: (0..n).collect(),
52 slots: (0..num_slots).collect(),
53 });
54 }
55 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);
58 if item_match.iter().all(|m| m.is_some()) {
59 return MatchOutcome::Feasible(item_match.into_iter().map(|m| m.unwrap()).collect());
60 }
61 MatchOutcome::Infeasible(extract_hall(adj, num_slots, &slot_match, &item_match))
62}
63
64fn hopcroft_karp(
68 adj: &[Vec<usize>],
69 slot_match: &mut [Option<usize>],
70 item_match: &mut [Option<usize>],
71) {
72 let n = adj.len();
73 let num_slots = slot_match.len();
74 const INF: usize = usize::MAX;
75 loop {
76 let mut dist = vec![INF; n];
78 let mut queue = std::collections::VecDeque::new();
79 for u in 0..n {
80 if item_match[u].is_none() {
81 dist[u] = 0;
82 queue.push_back(u);
83 }
84 }
85 let mut reachable_free = false;
86 while let Some(u) = queue.pop_front() {
87 for &v in &adj[u] {
88 if v >= num_slots {
89 continue;
90 }
91 match slot_match[v] {
92 None => reachable_free = true,
93 Some(w) if dist[w] == INF => {
94 dist[w] = dist[u] + 1;
95 queue.push_back(w);
96 }
97 _ => {}
98 }
99 }
100 }
101 if !reachable_free {
102 break; }
104 for u in 0..n {
105 if item_match[u].is_none() {
106 hk_dfs(u, adj, slot_match, item_match, &mut dist);
107 }
108 }
109 }
110}
111
112fn hk_dfs(
115 u: usize,
116 adj: &[Vec<usize>],
117 slot_match: &mut [Option<usize>],
118 item_match: &mut [Option<usize>],
119 dist: &mut [usize],
120) -> bool {
121 let num_slots = slot_match.len();
122 for idx in 0..adj[u].len() {
123 let v = adj[u][idx];
124 if v >= num_slots {
125 continue;
126 }
127 let proceed = match slot_match[v] {
128 None => true,
129 Some(w) => dist[w] == dist[u] + 1 && hk_dfs(w, adj, slot_match, item_match, dist),
130 };
131 if proceed {
132 slot_match[v] = Some(u);
133 item_match[u] = Some(v);
134 return true;
135 }
136 }
137 dist[u] = usize::MAX;
138 false
139}
140
141fn extract_hall(
147 adj: &[Vec<usize>],
148 num_slots: usize,
149 slot_match: &[Option<usize>],
150 item_match: &[Option<usize>],
151) -> HallWitness {
152 let n = adj.len();
153 let mut item_reach = vec![false; n];
154 let mut slot_reach = vec![false; num_slots];
155 let mut stack: Vec<usize> = Vec::new();
156 for (u, m) in item_match.iter().enumerate() {
157 if m.is_none() {
158 item_reach[u] = true;
159 stack.push(u);
160 }
161 }
162 while let Some(u) = stack.pop() {
163 for &v in &adj[u] {
164 if v < num_slots && !slot_reach[v] {
165 slot_reach[v] = true;
166 if let Some(w) = slot_match[v] {
167 if !item_reach[w] {
168 item_reach[w] = true;
169 stack.push(w);
170 }
171 }
172 }
173 }
174 }
175 HallWitness {
176 items: (0..n).filter(|&u| item_reach[u]).collect(),
177 slots: (0..num_slots).filter(|&v| slot_reach[v]).collect(),
178 }
179}
180
181pub fn is_hall_witness(adj: &[Vec<usize>], w: &HallWitness) -> bool {
185 if w.items.len() <= w.slots.len() {
186 return false;
187 }
188 let slot_set: std::collections::HashSet<usize> = w.slots.iter().copied().collect();
189 w.items.iter().all(|&i| {
190 i < adj.len() && adj[i].iter().all(|s| slot_set.contains(s))
191 })
192}
193
194pub fn is_valid_assignment(adj: &[Vec<usize>], num_slots: usize, assignment: &[usize]) -> bool {
196 if assignment.len() != adj.len() {
197 return false;
198 }
199 let mut used = vec![false; num_slots];
200 assignment.iter().enumerate().all(|(i, &s)| {
201 let ok = s < num_slots && adj[i].contains(&s) && !used[s];
202 if s < num_slots {
203 used[s] = true;
204 }
205 ok
206 })
207}
208
209#[derive(Clone, Debug, PartialEq, Eq)]
213pub enum CapMatchOutcome {
214 Feasible(Vec<usize>),
216 Infeasible(CapHallWitness),
218}
219
220#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct CapHallWitness {
225 pub items: Vec<usize>,
227 pub slots: Vec<usize>,
229}
230
231pub fn assign_or_hall_capacitated(adj: &[Vec<usize>], capacities: &[usize]) -> CapMatchOutcome {
236 let num_slots = capacities.len();
237 let mut offset = vec![0usize; num_slots + 1];
238 for s in 0..num_slots {
239 offset[s + 1] = offset[s] + capacities[s];
240 }
241 let total = offset[num_slots];
242 let exp_adj: Vec<Vec<usize>> = adj
244 .iter()
245 .map(|slots| {
246 slots
247 .iter()
248 .filter(|&&s| s < num_slots)
249 .flat_map(|&s| offset[s]..offset[s + 1])
250 .collect()
251 })
252 .collect();
253 let copy_to_slot = |c: usize| offset.partition_point(|&o| o <= c) - 1;
254 match assign_or_hall(&exp_adj, total) {
255 MatchOutcome::Feasible(copy_assign) => {
256 CapMatchOutcome::Feasible(copy_assign.into_iter().map(copy_to_slot).collect())
257 }
258 MatchOutcome::Infeasible(w) => {
259 let mut slots: Vec<usize> = w.slots.into_iter().map(copy_to_slot).collect();
262 slots.sort_unstable();
263 slots.dedup();
264 CapMatchOutcome::Infeasible(CapHallWitness { items: w.items, slots })
265 }
266 }
267}
268
269pub fn is_cap_hall_witness(adj: &[Vec<usize>], capacities: &[usize], w: &CapHallWitness) -> bool {
272 let cap: usize = w.slots.iter().map(|&s| capacities.get(s).copied().unwrap_or(0)).sum();
273 if w.items.len() <= cap {
274 return false;
275 }
276 let slot_set: std::collections::HashSet<usize> = w.slots.iter().copied().collect();
277 w.items
278 .iter()
279 .all(|&i| i < adj.len() && adj[i].iter().all(|s| slot_set.contains(s)))
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn php(n: usize) -> (Vec<Vec<usize>>, usize) {
289 let holes = n - 1;
290 ((0..n).map(|_| (0..holes).collect()).collect(), holes)
291 }
292
293 #[test]
294 fn pigeonhole_is_infeasible_with_a_genuine_hall_witness() {
295 for n in 2..=12 {
296 let (adj, slots) = php(n);
297 match assign_or_hall(&adj, slots) {
298 MatchOutcome::Infeasible(w) => {
299 assert!(is_hall_witness(&adj, &w), "PHP({n}) witness invalid: {w:?}");
300 assert_eq!(w.items.len(), n, "all {n} pigeons are deficient");
301 assert_eq!(w.slots.len(), n - 1, "against {} holes", n - 1);
302 }
303 other => panic!("PHP({n}) must be infeasible, got {other:?}"),
304 }
305 }
306 }
307
308 #[test]
309 fn equal_items_and_slots_is_feasible() {
310 for n in 1..=10 {
312 let adj: Vec<Vec<usize>> = (0..n).map(|_| (0..n).collect()).collect();
313 match assign_or_hall(&adj, n) {
314 MatchOutcome::Feasible(a) => {
315 assert!(is_valid_assignment(&adj, n, &a), "invalid assignment {a:?}");
316 }
317 other => panic!("n={n} square should be feasible, got {other:?}"),
318 }
319 }
320 }
321
322 #[test]
323 fn restricted_subset_triggers_hall() {
324 let adj = vec![vec![0, 1], vec![0, 1], vec![0, 1], vec![2, 3]];
326 match assign_or_hall(&adj, 4) {
327 MatchOutcome::Infeasible(w) => {
328 assert!(is_hall_witness(&adj, &w), "witness invalid: {w:?}");
329 assert!(w.items.len() > w.slots.len());
330 }
331 other => panic!("expected Hall violation, got {other:?}"),
332 }
333 }
334
335 #[test]
336 fn a_solvable_restricted_matching_is_feasible() {
337 let adj = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 0]];
339 match assign_or_hall(&adj, 4) {
340 MatchOutcome::Feasible(a) => assert!(is_valid_assignment(&adj, 4, &a)),
341 other => panic!("expected a feasible matching, got {other:?}"),
342 }
343 }
344
345 #[test]
346 fn empty_is_trivially_feasible() {
347 assert_eq!(assign_or_hall(&[], 0), MatchOutcome::Feasible(vec![]));
348 }
349
350 #[test]
351 fn a_bad_hall_witness_is_rejected() {
352 let adj = vec![vec![0, 1], vec![0, 1]];
354 assert!(!is_hall_witness(&adj, &HallWitness { items: vec![0], slots: vec![0, 1] }));
355 assert!(!is_hall_witness(&adj, &HallWitness { items: vec![0, 1], slots: vec![0] }),
356 "item 1 reaches slot 1 ∉ T, so {{0}} cannot cover N(S)");
357 }
358
359 #[test]
360 fn capacity_makes_overloaded_slots_infeasible() {
361 let adj = vec![vec![0], vec![0], vec![0], vec![0], vec![0]];
363 assert!(matches!(assign_or_hall_capacitated(&adj, &[5]), CapMatchOutcome::Feasible(_)));
364 match assign_or_hall_capacitated(&adj, &[4]) {
365 CapMatchOutcome::Infeasible(w) => {
366 assert!(is_cap_hall_witness(&adj, &[4], &w), "cap witness invalid: {w:?}");
367 assert_eq!(w.items.len(), 5);
368 assert_eq!(w.slots, vec![0]);
369 }
370 o => panic!("capacity 4 must be infeasible: {o:?}"),
371 }
372 }
373
374 #[test]
375 fn capacitated_assignment_respects_capacities() {
376 let adj: Vec<Vec<usize>> = (0..4).map(|_| vec![0, 1]).collect();
378 match assign_or_hall_capacitated(&adj, &[2, 2]) {
379 CapMatchOutcome::Feasible(a) => {
380 assert_eq!(a.len(), 4);
381 assert!(a.iter().filter(|&&s| s == 0).count() <= 2);
382 assert!(a.iter().filter(|&&s| s == 1).count() <= 2);
383 assert!(a.iter().all(|&s| s == 0 || s == 1));
384 }
385 o => panic!("should be feasible: {o:?}"),
386 }
387 let adj5: Vec<Vec<usize>> = (0..5).map(|_| vec![0, 1]).collect();
389 match assign_or_hall_capacitated(&adj5, &[2, 2]) {
390 CapMatchOutcome::Infeasible(w) => assert!(is_cap_hall_witness(&adj5, &[2, 2], &w)),
391 o => panic!("5 items / capacity 4 must be infeasible: {o:?}"),
392 }
393 }
394
395 #[test]
396 fn hopcroft_karp_finds_the_maximum_matching() {
397 fn kuhn_size(adj: &[Vec<usize>], num_slots: usize) -> usize {
399 fn aug(u: usize, adj: &[Vec<usize>], sm: &mut [Option<usize>], seen: &mut [bool]) -> bool {
400 for &v in &adj[u] {
401 if v >= seen.len() || seen[v] {
402 continue;
403 }
404 seen[v] = true;
405 if sm[v].is_none() || aug(sm[v].unwrap(), adj, sm, seen) {
406 sm[v] = Some(u);
407 return true;
408 }
409 }
410 false
411 }
412 let mut sm = vec![None; num_slots];
413 let mut count = 0;
414 for u in 0..adj.len() {
415 let mut seen = vec![false; num_slots];
416 if aug(u, adj, &mut sm, &mut seen) {
417 count += 1;
418 }
419 }
420 count
421 }
422 let mut s: u64 = 0x9E3779B97F4A7C15;
424 let mut next = || {
425 s ^= s << 13;
426 s ^= s >> 7;
427 s ^= s << 17;
428 s
429 };
430 for _ in 0..300 {
431 let n = (next() % 9) as usize + 1;
432 let num_slots = (next() % 9) as usize + 1;
433 let adj: Vec<Vec<usize>> = (0..n)
434 .map(|_| (0..num_slots).filter(|_| next() % 2 == 0).collect())
435 .collect();
436 let kuhn = kuhn_size(&adj, num_slots);
437 let outcome = assign_or_hall(&adj, num_slots);
438 let feasible = matches!(outcome, MatchOutcome::Feasible(_));
440 assert_eq!(
441 feasible,
442 kuhn == n,
443 "HK/Kuhn disagree: adj={adj:?} slots={num_slots} hk_feasible={feasible} kuhn={kuhn} n={n}"
444 );
445 match outcome {
447 MatchOutcome::Feasible(a) => assert!(is_valid_assignment(&adj, num_slots, &a)),
448 MatchOutcome::Infeasible(w) => assert!(is_hall_witness(&adj, &w)),
449 }
450 }
451 }
452}