pub fn greedy_match(
cost_matrix: &[Vec<f32>],
threshold: f32,
) -> (Vec<(usize, usize)>, Vec<usize>, Vec<usize>) {
if cost_matrix.is_empty() {
return (Vec::new(), Vec::new(), Vec::new());
}
let rows = cost_matrix.len();
let cols = cost_matrix[0].len();
let mut matches = Vec::new();
let mut row_matched = vec![false; rows];
let mut col_matched = vec![false; cols];
let mut costs: Vec<(f32, usize, usize)> = Vec::with_capacity(rows * cols);
for (r, row) in cost_matrix.iter().enumerate() {
for (c, &cost) in row.iter().enumerate() {
costs.push((cost, r, c));
}
}
costs.sort_by(|a, b| a.0.total_cmp(&b.0));
for (cost, r, c) in costs {
if cost > threshold {
break;
}
if !row_matched[r] && !col_matched[c] {
matches.push((r, c));
row_matched[r] = true;
col_matched[c] = true;
}
}
let unmatched_rows = (0..rows).filter(|&r| !row_matched[r]).collect();
let unmatched_cols = (0..cols).filter(|&c| !col_matched[c]).collect();
(matches, unmatched_rows, unmatched_cols)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_matrix_matches_nothing() {
let (m, ur, uc) = greedy_match(&[], 0.5);
assert!(m.is_empty());
assert!(ur.is_empty());
assert!(uc.is_empty());
}
#[test]
fn picks_lowest_cost_pairs() {
let cost = vec![vec![0.9, 0.1], vec![0.2, 0.8]];
let (mut m, ur, uc) = greedy_match(&cost, 0.5);
m.sort();
assert_eq!(m, vec![(0, 1), (1, 0)]);
assert!(ur.is_empty());
assert!(uc.is_empty());
}
#[test]
fn rejects_costs_above_threshold() {
let cost = vec![vec![0.9, 0.9], vec![0.9, 0.9]];
let (m, mut ur, mut uc) = greedy_match(&cost, 0.5);
ur.sort();
uc.sort();
assert!(m.is_empty());
assert_eq!(ur, vec![0, 1]);
assert_eq!(uc, vec![0, 1]);
}
#[test]
fn handles_more_rows_than_cols() {
let cost = vec![vec![0.1], vec![0.2], vec![0.3]];
let (m, mut ur, uc) = greedy_match(&cost, 1.0);
assert_eq!(m, vec![(0, 0)]);
ur.sort();
assert_eq!(ur, vec![1, 2]);
assert!(uc.is_empty());
}
}