mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
feat: implement Hungarian algorithm for optimal instance pairing in reconciler
- Replaced the previous greedy matching approach with the Hungarian (Kuhn-Munkres) algorithm for improved accuracy in instance pairing, especially in ambiguous groups with multiple same-named instances. - Introduced a new Hungarian.lua module to encapsulate the algorithm's logic. - Updated the reconciler to utilize the new optimal assignment function, enhancing overall matching performance. - Documented changes in the CHANGELOG for clarity on the new implementation.
This commit is contained in:
parent
3f469d615b
commit
372e6bc739
7 changed files with 234 additions and 287 deletions
|
|
@ -28,93 +28,6 @@ const UNMATCHED_PENALTY: u32 = 10_000;
|
|||
/// O(n^k) explosion on deeply nested trees with many same-named instances.
|
||||
const MAX_SCORING_DEPTH: u32 = 3;
|
||||
|
||||
/// Minimum-cost bipartite matching via the Hungarian (Kuhn-Munkres) algorithm.
|
||||
/// See `src/syncback/matching.rs` for detailed documentation.
|
||||
fn min_cost_assignment(cost: &[Vec<u32>], rows: usize, cols: usize) -> Vec<(usize, usize)> {
|
||||
if rows == 0 || cols == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let n = rows.max(cols);
|
||||
let big = UNMATCHED_PENALTY.saturating_mul(2);
|
||||
|
||||
let mut c = vec![vec![0i64; n]; n];
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
c[i][j] = if i < rows && j < cols {
|
||||
cost[i][j] as i64
|
||||
} else {
|
||||
big as i64
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut u = vec![0i64; n + 1];
|
||||
let mut v = vec![0i64; n + 1];
|
||||
let mut p = vec![0usize; n + 1];
|
||||
let mut way = vec![0usize; n + 1];
|
||||
|
||||
for i in 1..=n {
|
||||
p[0] = i;
|
||||
let mut j0 = 0usize;
|
||||
let mut min_v = vec![i64::MAX; n + 1];
|
||||
let mut used = vec![false; n + 1];
|
||||
|
||||
loop {
|
||||
used[j0] = true;
|
||||
let i0 = p[j0];
|
||||
let mut delta = i64::MAX;
|
||||
let mut j1 = 0usize;
|
||||
|
||||
for j in 1..=n {
|
||||
if used[j] {
|
||||
continue;
|
||||
}
|
||||
let cur = c[i0 - 1][j - 1] - u[i0] - v[j];
|
||||
if cur < min_v[j] {
|
||||
min_v[j] = cur;
|
||||
way[j] = j0;
|
||||
}
|
||||
if min_v[j] < delta {
|
||||
delta = min_v[j];
|
||||
j1 = j;
|
||||
}
|
||||
}
|
||||
|
||||
for j in 0..=n {
|
||||
if used[j] {
|
||||
u[p[j]] += delta;
|
||||
v[j] -= delta;
|
||||
} else {
|
||||
min_v[j] -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
j0 = j1;
|
||||
if p[j0] == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let j1 = way[j0];
|
||||
p[j0] = p[j1];
|
||||
j0 = j1;
|
||||
if j0 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(rows.min(cols));
|
||||
for j in 1..=n {
|
||||
if p[j] != 0 && p[j] <= rows && j <= cols {
|
||||
result.push((p[j] - 1, j - 1));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Session-scoped cache for the matching algorithm. Caches
|
||||
/// `compute_change_count` results so that recursive scoring work is
|
||||
/// reused across calls at different tree levels.
|
||||
|
|
@ -262,7 +175,12 @@ pub fn match_forward(
|
|||
cost_matrix.push(row);
|
||||
}
|
||||
|
||||
let assignment = min_cost_assignment(&cost_matrix, m, n);
|
||||
let assignment = crate::hungarian::min_cost_assignment(
|
||||
&cost_matrix,
|
||||
m,
|
||||
n,
|
||||
UNMATCHED_PENALTY.saturating_mul(2),
|
||||
);
|
||||
for (row, col) in assignment {
|
||||
let si = avail_snap[row];
|
||||
let ti = avail_tree[col];
|
||||
|
|
@ -419,7 +337,12 @@ fn match_children_for_scoring(
|
|||
cost_matrix.push(row);
|
||||
}
|
||||
|
||||
let assignment = min_cost_assignment(&cost_matrix, m, n);
|
||||
let assignment = crate::hungarian::min_cost_assignment(
|
||||
&cost_matrix,
|
||||
m,
|
||||
n,
|
||||
UNMATCHED_PENALTY.saturating_mul(2),
|
||||
);
|
||||
for (row, col) in assignment {
|
||||
let si = avail_snap[row];
|
||||
let ti = avail_tree[col];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue