1use crate::error::OptimizeError;
7use scirs2_core::ndarray::Array2;
8
9pub type AssignmentResult<T> = Result<T, OptimizeError>;
11
12pub fn hungarian_algorithm(cost: &Array2<f64>) -> AssignmentResult<(Vec<usize>, f64)> {
28 let rows = cost.nrows();
29 let cols = cost.ncols();
30 if rows == 0 || cols == 0 {
31 return Err(OptimizeError::InvalidInput(
32 "Cost matrix must be non-empty".to_string(),
33 ));
34 }
35
36 let n = rows.max(cols);
38 let mut c = vec![vec![0.0f64; n]; n];
39 for i in 0..rows {
40 for j in 0..cols {
41 c[i][j] = cost[[i, j]];
42 }
43 }
44
45 for row in c.iter_mut() {
47 let min_val = row.iter().cloned().fold(f64::INFINITY, f64::min);
48 if min_val.is_finite() {
49 for x in row.iter_mut() {
50 *x -= min_val;
51 }
52 }
53 }
54
55 for j in 0..n {
57 let min_val = (0..n).map(|i| c[i][j]).fold(f64::INFINITY, f64::min);
58 if min_val.is_finite() {
59 for i in 0..n {
60 c[i][j] -= min_val;
61 }
62 }
63 }
64
65 let mut row_assign = vec![usize::MAX; n]; let mut col_assign = vec![usize::MAX; n]; loop {
70 row_assign = vec![usize::MAX; n];
72 col_assign = vec![usize::MAX; n];
73 greedy_assign_zeros(&c, &mut row_assign, &mut col_assign, n);
74
75 let assigned = row_assign.iter().filter(|&&x| x != usize::MAX).count();
77 if assigned == n {
78 break;
79 }
80
81 let (row_covered, col_covered) = min_line_cover(&c, &row_assign, &col_assign, n);
83
84 let mut min_uncovered = f64::INFINITY;
86 for i in 0..n {
87 if row_covered[i] {
88 continue;
89 }
90 for j in 0..n {
91 if !col_covered[j] && c[i][j] < min_uncovered {
92 min_uncovered = c[i][j];
93 }
94 }
95 }
96
97 if !min_uncovered.is_finite() || min_uncovered <= 0.0 {
98 break; }
100
101 for i in 0..n {
103 for j in 0..n {
104 if !row_covered[i] && !col_covered[j] {
105 c[i][j] -= min_uncovered;
106 } else if row_covered[i] && col_covered[j] {
107 c[i][j] += min_uncovered;
108 }
109 }
110 }
111 }
112
113 row_assign = vec![usize::MAX; n];
115 col_assign = vec![usize::MAX; n];
116 for i in 0..n {
117 let mut visited_cols = vec![false; n];
118 augment_assignment(
119 i,
120 &c,
121 &mut row_assign,
122 &mut col_assign,
123 &mut visited_cols,
124 n,
125 );
126 }
127
128 let mut total_cost = 0.0;
132 let mut assignment = vec![0usize; rows];
133 for i in 0..rows {
134 let j = row_assign[i];
135 assignment[i] = j; if j < cols {
137 total_cost += cost[[i, j]];
138 }
139 }
140
141 Ok((assignment, total_cost))
142}
143
144fn greedy_assign_zeros(
146 c: &[Vec<f64>],
147 row_assign: &mut Vec<usize>,
148 col_assign: &mut Vec<usize>,
149 n: usize,
150) {
151 let mut changed = true;
153 while changed {
154 changed = false;
155 for i in 0..n {
156 if row_assign[i] != usize::MAX {
157 continue;
158 }
159 let zeros: Vec<usize> = (0..n)
160 .filter(|&j| col_assign[j] == usize::MAX && c[i][j].abs() < 1e-10)
161 .collect();
162 if zeros.len() == 1 {
163 row_assign[i] = zeros[0];
164 col_assign[zeros[0]] = i;
165 changed = true;
166 }
167 }
168 }
169 for i in 0..n {
171 if row_assign[i] != usize::MAX {
172 continue;
173 }
174 for j in 0..n {
175 if col_assign[j] == usize::MAX && c[i][j].abs() < 1e-10 {
176 row_assign[i] = j;
177 col_assign[j] = i;
178 break;
179 }
180 }
181 }
182}
183
184fn min_line_cover(
191 c: &[Vec<f64>],
192 row_assign: &[usize],
193 col_assign: &[usize],
194 n: usize,
195) -> (Vec<bool>, Vec<bool>) {
196 let mut marked_rows = vec![false; n];
197 let mut marked_cols = vec![false; n];
198
199 for i in 0..n {
201 if row_assign[i] == usize::MAX {
202 marked_rows[i] = true;
203 }
204 }
205
206 let mut changed = true;
207 while changed {
208 changed = false;
209 for i in 0..n {
211 if !marked_rows[i] {
212 continue;
213 }
214 for j in 0..n {
215 if !marked_cols[j] && c[i][j].abs() < 1e-10 {
216 marked_cols[j] = true;
217 changed = true;
218 let r = col_assign[j];
220 if r != usize::MAX && !marked_rows[r] {
221 marked_rows[r] = true;
222 }
223 }
224 }
225 }
226 }
227
228 let row_covered: Vec<bool> = marked_rows.iter().map(|&m| !m).collect();
230 let col_covered = marked_cols;
231 (row_covered, col_covered)
232}
233
234fn augment_assignment(
236 row: usize,
237 c: &[Vec<f64>],
238 row_assign: &mut Vec<usize>,
239 col_assign: &mut Vec<usize>,
240 visited_cols: &mut Vec<bool>,
241 n: usize,
242) -> bool {
243 for j in 0..n {
244 if visited_cols[j] || c[row][j].abs() >= 1e-10 {
245 continue;
246 }
247 visited_cols[j] = true;
248 let prev = col_assign[j];
249 if prev == usize::MAX
250 || augment_assignment(prev, c, row_assign, col_assign, visited_cols, n)
251 {
252 row_assign[row] = j;
253 col_assign[j] = row;
254 return true;
255 }
256 }
257 false
258}
259
260pub fn min_cost_matching(
274 n: usize,
275 m: usize,
276 edges: &[(usize, usize, f64)],
277) -> AssignmentResult<(Vec<Option<usize>>, f64)> {
278 if n == 0 || m == 0 {
279 return Ok((vec![None; n], 0.0));
280 }
281
282 for &(u, v, _) in edges {
284 if u >= n {
285 return Err(OptimizeError::InvalidInput(format!(
286 "Left vertex {u} out of range [0, {n})"
287 )));
288 }
289 if v >= m {
290 return Err(OptimizeError::InvalidInput(format!(
291 "Right vertex {v} out of range [0, {m})"
292 )));
293 }
294 }
295
296 let dim = n.max(m);
298 let mut cost_mat = Array2::<f64>::from_elem((dim, dim), f64::INFINITY);
299 for &(u, v, c) in edges {
300 if c < cost_mat[[u, v]] {
302 cost_mat[[u, v]] = c;
303 }
304 }
305
306 let max_finite = edges
308 .iter()
309 .map(|&(_, _, c)| c.abs())
310 .fold(0.0f64, f64::max);
311 let large = (max_finite + 1.0) * dim as f64 * 10.0;
312 for i in 0..dim {
313 for j in 0..dim {
314 if cost_mat[[i, j]].is_infinite() {
315 cost_mat[[i, j]] = large;
316 }
317 }
318 }
319
320 let (raw_assign, _) = hungarian_algorithm(&cost_mat)?;
321
322 let mut assignment = vec![None; n];
324 let mut total_cost = 0.0;
325
326 for i in 0..n {
327 let j = raw_assign[i];
328 if j < m && cost_mat[[i, j]] < large - 1e-9 {
329 assignment[i] = Some(j);
330 total_cost += cost_mat[[i, j]];
331 }
332 }
333
334 Ok((assignment, total_cost))
335}
336
337#[cfg(test)]
341mod tests {
342 use super::*;
343 use scirs2_core::ndarray::array;
344
345 fn verify_assignment(cost: &Array2<f64>, assignment: &[usize], total: f64) {
347 let n = assignment.len();
348 let mut seen = vec![false; cost.ncols().max(n)];
349 for (i, &j) in assignment.iter().enumerate() {
350 assert!(!seen[j], "column {j} assigned twice");
351 seen[j] = true;
352 let _ = cost[[i, j]]; }
354 let computed: f64 = assignment
355 .iter()
356 .enumerate()
357 .map(|(i, &j)| cost[[i, j]])
358 .sum();
359 assert!(
360 (computed - total).abs() < 1e-6,
361 "cost mismatch: {computed} vs {total}"
362 );
363 }
364
365 #[test]
366 fn test_hungarian_3x3() {
367 let cost = array![[4.0, 1.0, 3.0], [2.0, 0.0, 5.0], [3.0, 2.0, 2.0]];
369 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
370 verify_assignment(&cost, &assign, total);
371 assert!((total - 5.0).abs() < 1e-6, "expected 5.0 got {total}");
373 }
374
375 #[test]
376 fn test_hungarian_4x4_known_optimal() {
377 let cost = array![
379 [9.0, 2.0, 7.0, 8.0],
380 [6.0, 4.0, 3.0, 7.0],
381 [5.0, 8.0, 1.0, 8.0],
382 [7.0, 6.0, 9.0, 4.0]
383 ];
384 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
385 verify_assignment(&cost, &assign, total);
386 assert!(total <= 15.0 + 1e-6, "expected ≤15 got {total}");
387 }
388
389 #[test]
390 fn test_hungarian_identity() {
391 let cost = array![[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
393 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
394 verify_assignment(&cost, &assign, total);
395 assert!((total - 0.0).abs() < 1e-6);
396 }
397
398 #[test]
399 fn test_hungarian_1x1() {
400 let cost = array![[7.0]];
401 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
402 assert_eq!(assign, vec![0]);
403 assert!((total - 7.0).abs() < 1e-6);
404 }
405
406 #[test]
407 fn test_hungarian_2x2() {
408 let cost = array![[4.0, 3.0], [3.0, 4.0]];
409 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
410 verify_assignment(&cost, &assign, total);
411 assert!((total - 6.0).abs() < 1e-6, "expected 6.0 got {total}");
412 }
413
414 #[test]
415 fn test_hungarian_empty_error() {
416 let cost: Array2<f64> = Array2::zeros((0, 0));
417 assert!(hungarian_algorithm(&cost).is_err());
418 }
419
420 #[test]
421 fn test_min_cost_matching_basic() {
422 let edges = vec![(0, 0, 1.0), (0, 1, 4.0), (1, 0, 2.0), (1, 1, 3.0)];
425 let (assign, total) = min_cost_matching(2, 2, &edges).expect("unexpected None or Err");
426 assert_eq!(assign[0], Some(0));
427 assert_eq!(assign[1], Some(1));
428 assert!((total - 4.0).abs() < 1e-6, "expected 4.0 got {total}");
429 }
430
431 #[test]
432 fn test_min_cost_matching_sparse() {
433 let edges = vec![(0, 0, 1.0), (1, 1, 2.0)];
435 let (assign, total) = min_cost_matching(2, 2, &edges).expect("unexpected None or Err");
436 assert_eq!(assign[0], Some(0));
437 assert_eq!(assign[1], Some(1));
438 assert!((total - 3.0).abs() < 1e-6);
439 }
440
441 #[test]
442 fn test_min_cost_matching_invalid_vertex() {
443 let edges = vec![(0, 5, 1.0)]; assert!(min_cost_matching(2, 3, &edges).is_err());
445 }
446
447 #[test]
448 fn test_hungarian_rectangular_3x2() {
449 let cost = array![[5.0, 2.0], [3.0, 4.0], [1.0, 6.0]];
451 let (assign, total) = hungarian_algorithm(&cost).expect("unexpected None or Err");
452 assert_eq!(assign.len(), 3);
454 let real_assigns: Vec<usize> = assign.iter().cloned().filter(|&j| j < 2).collect();
456 assert_eq!(
457 real_assigns.len(),
458 2,
459 "expected exactly 2 real-column assignments"
460 );
461 let _ = total;
462 }
463}