use std::collections::VecDeque;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct MatchingResult {
pub col_to_row: Vec<i32>,
pub row_to_col: Vec<i32>,
pub matching_size: usize,
pub structural_rank: usize,
}
const NIL: i32 = -1;
const INF: i32 = i32::MAX;
pub fn hopcroft_karp(
n_rows: usize,
n_cols: usize,
col_ptrs: &[i64],
row_indices: &[i64],
) -> Result<MatchingResult> {
if col_ptrs.len() != n_cols + 1 {
return Err(Error::InvalidArgument {
arg: "col_ptrs",
reason: format!(
"length {} does not match n_cols + 1 = {}",
col_ptrs.len(),
n_cols + 1
),
});
}
let nnz = col_ptrs[n_cols] as usize;
if row_indices.len() < nnz {
return Err(Error::InvalidArgument {
arg: "row_indices",
reason: format!("length {} is less than nnz = {}", row_indices.len(), nnz),
});
}
if n_rows == 0 || n_cols == 0 {
return Ok(MatchingResult {
col_to_row: vec![NIL; n_cols],
row_to_col: vec![NIL; n_rows],
matching_size: 0,
structural_rank: 0,
});
}
let mut col_to_row: Vec<i32> = vec![NIL; n_cols];
let mut row_to_col: Vec<i32> = vec![NIL; n_rows];
let mut dist: Vec<i32> = vec![0; n_cols + 1];
let mut matching_size = 0usize;
while bfs(
n_rows,
n_cols,
col_ptrs,
row_indices,
&col_to_row,
&row_to_col,
&mut dist,
) {
for j in 0..n_cols {
if col_to_row[j] == NIL
&& dfs(
j,
n_rows,
n_cols,
col_ptrs,
row_indices,
&mut col_to_row,
&mut row_to_col,
&mut dist,
)
{
matching_size += 1;
}
}
}
Ok(MatchingResult {
col_to_row,
row_to_col,
matching_size,
structural_rank: matching_size,
})
}
fn bfs(
n_rows: usize,
n_cols: usize,
col_ptrs: &[i64],
row_indices: &[i64],
col_to_row: &[i32],
row_to_col: &[i32],
dist: &mut [i32],
) -> bool {
let mut queue: VecDeque<usize> = VecDeque::with_capacity(n_cols);
for j in 0..n_cols {
if col_to_row[j] == NIL {
dist[j] = 0;
queue.push_back(j);
} else {
dist[j] = INF;
}
}
dist[n_cols] = INF;
while let Some(j) = queue.pop_front() {
if dist[j] < dist[n_cols] {
let start = col_ptrs[j] as usize;
let end = col_ptrs[j + 1] as usize;
for idx in start..end {
let i = row_indices[idx] as usize;
if i >= n_rows {
continue;
}
let matched_col = row_to_col[i];
let matched_idx = if matched_col == NIL {
n_cols } else {
matched_col as usize
};
if dist[matched_idx] == INF {
dist[matched_idx] = dist[j] + 1;
if matched_col != NIL {
queue.push_back(matched_col as usize);
}
}
}
}
}
dist[n_cols] != INF
}
fn dfs(
j: usize,
n_rows: usize,
n_cols: usize,
col_ptrs: &[i64],
row_indices: &[i64],
col_to_row: &mut [i32],
row_to_col: &mut [i32],
dist: &mut [i32],
) -> bool {
let start = col_ptrs[j] as usize;
let end = col_ptrs[j + 1] as usize;
for idx in start..end {
let i = row_indices[idx] as usize;
if i >= n_rows {
continue;
}
let matched_col = row_to_col[i];
let matched_idx = if matched_col == NIL {
n_cols
} else {
matched_col as usize
};
if dist[matched_idx] == dist[j] + 1 {
if matched_col == NIL
|| dfs(
matched_col as usize,
n_rows,
n_cols,
col_ptrs,
row_indices,
col_to_row,
row_to_col,
dist,
)
{
col_to_row[j] = i as i32;
row_to_col[i] = j as i32;
return true;
}
}
}
dist[j] = INF;
false
}
pub fn maximum_transversal(
n_rows: usize,
n_cols: usize,
col_ptrs: &[i64],
row_indices: &[i64],
) -> Result<(Vec<i32>, Vec<i32>, usize)> {
let result = hopcroft_karp(n_rows, n_cols, col_ptrs, row_indices)?;
Ok((result.col_to_row, result.row_to_col, result.structural_rank))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hopcroft_karp_empty() {
let col_ptrs = vec![0i64];
let row_indices: Vec<i64> = vec![];
let result = hopcroft_karp(0, 0, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 0);
assert_eq!(result.structural_rank, 0);
}
#[test]
fn test_hopcroft_karp_single_edge() {
let col_ptrs = vec![0i64, 1];
let row_indices = vec![0i64];
let result = hopcroft_karp(1, 1, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 1);
assert_eq!(result.col_to_row[0], 0);
assert_eq!(result.row_to_col[0], 0);
}
#[test]
fn test_hopcroft_karp_diagonal() {
let col_ptrs = vec![0i64, 1, 2, 3];
let row_indices = vec![0i64, 1, 2];
let result = hopcroft_karp(3, 3, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 3);
assert_eq!(result.structural_rank, 3);
assert_eq!(result.col_to_row[0], 0);
assert_eq!(result.col_to_row[1], 1);
assert_eq!(result.col_to_row[2], 2);
}
#[test]
fn test_hopcroft_karp_permutation_needed() {
let col_ptrs = vec![0i64, 1, 3];
let row_indices = vec![1i64, 0, 1];
let result = hopcroft_karp(2, 2, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 2);
assert_eq!(result.col_to_row[0], 1);
assert_eq!(result.col_to_row[1], 0);
}
#[test]
fn test_hopcroft_karp_incomplete_matching() {
let col_ptrs = vec![0i64, 2, 2];
let row_indices = vec![0i64, 1];
let result = hopcroft_karp(2, 2, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 1);
assert!(result.col_to_row[0] == 0 || result.col_to_row[0] == 1);
assert_eq!(result.col_to_row[1], NIL);
}
#[test]
fn test_hopcroft_karp_rectangular_more_rows() {
let col_ptrs = vec![0i64, 2, 5];
let row_indices = vec![0i64, 1, 1, 2, 3];
let result = hopcroft_karp(4, 2, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 2);
}
#[test]
fn test_hopcroft_karp_rectangular_more_cols() {
let col_ptrs = vec![0i64, 1, 2, 2, 3];
let row_indices = vec![0i64, 1, 0];
let result = hopcroft_karp(2, 4, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 2);
}
#[test]
fn test_hopcroft_karp_augmenting_path() {
let col_ptrs = vec![0i64, 2, 4, 6];
let row_indices = vec![0i64, 1, 0, 2, 1, 2];
let result = hopcroft_karp(3, 3, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 3);
assert_eq!(result.structural_rank, 3);
let mut row_matched = vec![false; 3];
for j in 0..3 {
let i = result.col_to_row[j];
assert!(i >= 0 && i < 3);
assert!(!row_matched[i as usize], "Row {} matched twice", i);
row_matched[i as usize] = true;
}
}
#[test]
fn test_maximum_transversal() {
let col_ptrs = vec![0i64, 1, 2, 3];
let row_indices = vec![0i64, 1, 2];
let (col_to_row, row_to_col, rank) =
maximum_transversal(3, 3, &col_ptrs, &row_indices).unwrap();
assert_eq!(rank, 3);
assert_eq!(col_to_row.len(), 3);
assert_eq!(row_to_col.len(), 3);
for (j, &i) in col_to_row.iter().enumerate() {
if i >= 0 {
assert_eq!(
row_to_col[i as usize], j as i32,
"Bidirectional matching must hold: col {} -> row {}, but row {} -> col {}",
j, i, i, row_to_col[i as usize]
);
}
}
}
#[test]
fn test_structural_rank_singular() {
let col_ptrs = vec![0i64, 2, 4];
let row_indices = vec![0i64, 1, 0, 1];
let result = hopcroft_karp(2, 2, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.matching_size, 2);
}
#[test]
fn test_structural_rank_truly_singular() {
let col_ptrs = vec![0i64, 2, 2];
let row_indices = vec![0i64, 1];
let result = hopcroft_karp(2, 2, &col_ptrs, &row_indices).unwrap();
assert_eq!(result.structural_rank, 1);
}
}