mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +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
|
|
@ -31,6 +31,7 @@ Making a new release? Simply add the new header with the version and date undern
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
* Replace greedy matching with optimal Hungarian (Kuhn-Munkres) algorithm in forward sync, syncback, and plugin reconciler for correct instance pairing when multiple same-named instances exist in ambiguous groups
|
||||||
* Optimize syncback performance for large projects (~42% faster): skip hash computation in clean mode, reuse prefetch walkdir for orphan detection, rewrite collect_all_paths as O(n) top-down BFS, add file-size-first comparison in write phase, scope git index queries to project directory ([#10])
|
* Optimize syncback performance for large projects (~42% faster): skip hash computation in clean mode, reuse prefetch walkdir for orphan detection, rewrite collect_all_paths as O(n) top-down BFS, add file-size-first comparison in write phase, scope git index queries to project directory ([#10])
|
||||||
|
|
||||||
[#10]: https://github.com/UserGeneratedLLC/rojo/pull/10
|
[#10]: https://github.com/UserGeneratedLLC/rojo/pull/10
|
||||||
|
|
|
||||||
103
plugin/src/Hungarian.lua
Normal file
103
plugin/src/Hungarian.lua
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--[[
|
||||||
|
Minimum-cost bipartite matching via the Hungarian (Kuhn-Munkres) algorithm.
|
||||||
|
|
||||||
|
Given a 1-indexed cost matrix cost[i][j] for `rows` row-agents and `cols`
|
||||||
|
column-agents, returns an array of {row, col} pairs representing the
|
||||||
|
optimal assignment that minimizes total cost. Handles rectangular matrices
|
||||||
|
by padding to square with `padCost`.
|
||||||
|
|
||||||
|
Complexity: O(N^3) where N = max(rows, cols).
|
||||||
|
]]
|
||||||
|
|
||||||
|
local function minCostAssignment(cost: { { number } }, rows: number, cols: number, padCost: number): { { number } }
|
||||||
|
if rows == 0 or cols == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local n = math.max(rows, cols)
|
||||||
|
local S = n + 1
|
||||||
|
local INF = math.huge
|
||||||
|
|
||||||
|
local c: { { number } } = table.create(n)
|
||||||
|
for i = 1, n do
|
||||||
|
local row = table.create(n, padCost)
|
||||||
|
for j = 1, n do
|
||||||
|
if i <= rows and j <= cols then
|
||||||
|
row[j] = cost[i][j]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
c[i] = row
|
||||||
|
end
|
||||||
|
|
||||||
|
local u = table.create(S, 0)
|
||||||
|
local v = table.create(S, 0)
|
||||||
|
local p = table.create(S, 0)
|
||||||
|
local way = table.create(S, 0)
|
||||||
|
|
||||||
|
for i = 1, n do
|
||||||
|
p[S] = i
|
||||||
|
local j0 = S
|
||||||
|
local minV = table.create(S, INF)
|
||||||
|
local used = table.create(S, false)
|
||||||
|
|
||||||
|
while true do
|
||||||
|
used[j0] = true
|
||||||
|
local i0 = p[j0]
|
||||||
|
local delta = INF
|
||||||
|
local j1 = S
|
||||||
|
|
||||||
|
for j = 1, n do
|
||||||
|
if used[j] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
local cur = c[i0][j] - u[i0] - v[j]
|
||||||
|
if cur < minV[j] then
|
||||||
|
minV[j] = cur
|
||||||
|
way[j] = j0
|
||||||
|
end
|
||||||
|
if minV[j] < delta then
|
||||||
|
delta = minV[j]
|
||||||
|
j1 = j
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
u[p[S]] += delta
|
||||||
|
v[S] -= delta
|
||||||
|
for j = 1, n do
|
||||||
|
if used[j] then
|
||||||
|
u[p[j]] += delta
|
||||||
|
v[j] -= delta
|
||||||
|
else
|
||||||
|
minV[j] -= delta
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
j0 = j1
|
||||||
|
if p[j0] == 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local j1 = way[j0]
|
||||||
|
p[j0] = p[j1]
|
||||||
|
j0 = j1
|
||||||
|
if j0 == S then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local result: { { number } } = {}
|
||||||
|
for j = 1, n do
|
||||||
|
if p[j] ~= 0 and p[j] <= rows and j <= cols then
|
||||||
|
table.insert(result, { p[j], j })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
return minCostAssignment
|
||||||
|
|
@ -23,6 +23,7 @@ local Packages = script.Parent.Parent.Parent.Packages
|
||||||
local RbxDom = require(Packages.RbxDom)
|
local RbxDom = require(Packages.RbxDom)
|
||||||
|
|
||||||
local trueEquals = require(script.Parent.trueEquals)
|
local trueEquals = require(script.Parent.trueEquals)
|
||||||
|
local minCostAssignment = require(script.Parent.Parent.Hungarian)
|
||||||
|
|
||||||
local UNMATCHED_PENALTY = 10000
|
local UNMATCHED_PENALTY = 10000
|
||||||
local MAX_SCORING_DEPTH = 3
|
local MAX_SCORING_DEPTH = 3
|
||||||
|
|
@ -326,104 +327,6 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom
|
||||||
return cost
|
return cost
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ================================================================
|
|
||||||
-- Optimal assignment via Hungarian (Kuhn-Munkres) algorithm
|
|
||||||
-- ================================================================
|
|
||||||
|
|
||||||
local function minCostAssignment(cost: { { number } }, rows: number, cols: number): { { number } }
|
|
||||||
if rows == 0 or cols == 0 then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local n = math.max(rows, cols)
|
|
||||||
local big = UNMATCHED_PENALTY * 2
|
|
||||||
local S = n + 1 -- sentinel column (replaces 0-index from the Rust version)
|
|
||||||
local INF = math.huge
|
|
||||||
|
|
||||||
local c: { { number } } = table.create(n)
|
|
||||||
for i = 1, n do
|
|
||||||
local row = table.create(n, big)
|
|
||||||
for j = 1, n do
|
|
||||||
if i <= rows and j <= cols then
|
|
||||||
row[j] = cost[i][j]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
c[i] = row
|
|
||||||
end
|
|
||||||
|
|
||||||
-- u[1..n] = row potentials, u[S] unused but allocated for uniform indexing
|
|
||||||
local u = table.create(S, 0)
|
|
||||||
-- v[1..n] = col potentials, v[S] = sentinel potential
|
|
||||||
local v = table.create(S, 0)
|
|
||||||
-- p[j] = row assigned to column j (0 = unassigned), p[S] = sentinel
|
|
||||||
local p = table.create(S, 0)
|
|
||||||
-- way[j] = previous column in augmenting path
|
|
||||||
local way = table.create(S, 0)
|
|
||||||
|
|
||||||
for i = 1, n do
|
|
||||||
p[S] = i
|
|
||||||
local j0 = S
|
|
||||||
local minV = table.create(S, INF)
|
|
||||||
local used = table.create(S, false)
|
|
||||||
|
|
||||||
while true do
|
|
||||||
used[j0] = true
|
|
||||||
local i0 = p[j0]
|
|
||||||
local delta = INF
|
|
||||||
local j1 = S
|
|
||||||
|
|
||||||
for j = 1, n do
|
|
||||||
if used[j] then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
local cur = c[i0][j] - u[i0] - v[j]
|
|
||||||
if cur < minV[j] then
|
|
||||||
minV[j] = cur
|
|
||||||
way[j] = j0
|
|
||||||
end
|
|
||||||
if minV[j] < delta then
|
|
||||||
delta = minV[j]
|
|
||||||
j1 = j
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Update potentials: sentinel + real columns
|
|
||||||
u[p[S]] += delta
|
|
||||||
v[S] -= delta
|
|
||||||
for j = 1, n do
|
|
||||||
if used[j] then
|
|
||||||
u[p[j]] += delta
|
|
||||||
v[j] -= delta
|
|
||||||
else
|
|
||||||
minV[j] -= delta
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
j0 = j1
|
|
||||||
if p[j0] == 0 then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local j1 = way[j0]
|
|
||||||
p[j0] = p[j1]
|
|
||||||
j0 = j1
|
|
||||||
if j0 == S then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local result: { { number } } = {}
|
|
||||||
for j = 1, n do
|
|
||||||
if p[j] ~= 0 and p[j] <= rows and j <= cols then
|
|
||||||
table.insert(result, { p[j], j })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ================================================================
|
-- ================================================================
|
||||||
-- Core: 2-function mutual recursion
|
-- Core: 2-function mutual recursion
|
||||||
-- ================================================================
|
-- ================================================================
|
||||||
|
|
@ -666,7 +569,7 @@ matchChildren = function(
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Optimal assignment via the Hungarian algorithm.
|
-- Optimal assignment via the Hungarian algorithm.
|
||||||
local assignment = minCostAssignment(costMatrix, m, n)
|
local assignment = minCostAssignment(costMatrix, m, n, UNMATCHED_PENALTY * 2)
|
||||||
for _, pair in ipairs(assignment) do
|
for _, pair in ipairs(assignment) do
|
||||||
local vi = vIndices[pair[1]]
|
local vi = vIndices[pair[1]]
|
||||||
local si = sIndices[pair[2]]
|
local si = sIndices[pair[2]]
|
||||||
|
|
|
||||||
103
src/hungarian.rs
Normal file
103
src/hungarian.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
//! Minimum-cost bipartite matching via the Hungarian (Kuhn-Munkres) algorithm.
|
||||||
|
//!
|
||||||
|
//! Given a cost matrix `cost[i][j]` for `rows` row-agents and `cols`
|
||||||
|
//! column-agents, returns a Vec of `(row, col)` pairs representing the
|
||||||
|
//! optimal assignment that minimizes total cost. Handles rectangular
|
||||||
|
//! matrices by padding to square with `pad_cost`.
|
||||||
|
//!
|
||||||
|
//! Complexity: O(N^3) where N = max(rows, cols).
|
||||||
|
|
||||||
|
/// Compute the minimum-cost assignment for a (possibly rectangular) cost matrix.
|
||||||
|
///
|
||||||
|
/// `pad_cost` fills dummy entries when the matrix is padded to square.
|
||||||
|
/// It should be larger than any real cost in the matrix so that dummy
|
||||||
|
/// assignments are never preferred over real ones.
|
||||||
|
pub fn min_cost_assignment(
|
||||||
|
cost: &[Vec<u32>],
|
||||||
|
rows: usize,
|
||||||
|
cols: usize,
|
||||||
|
pad_cost: u32,
|
||||||
|
) -> Vec<(usize, usize)> {
|
||||||
|
if rows == 0 || cols == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = rows.max(cols);
|
||||||
|
let big = pad_cost as i64;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ mod tree_view;
|
||||||
mod change_processor;
|
mod change_processor;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
mod glob;
|
mod glob;
|
||||||
|
pub mod hungarian;
|
||||||
mod json;
|
mod json;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
mod lua_ast;
|
mod lua_ast;
|
||||||
|
|
|
||||||
|
|
@ -28,93 +28,6 @@ const UNMATCHED_PENALTY: u32 = 10_000;
|
||||||
/// O(n^k) explosion on deeply nested trees with many same-named instances.
|
/// O(n^k) explosion on deeply nested trees with many same-named instances.
|
||||||
const MAX_SCORING_DEPTH: u32 = 3;
|
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
|
/// Session-scoped cache for the matching algorithm. Caches
|
||||||
/// `compute_change_count` results so that recursive scoring work is
|
/// `compute_change_count` results so that recursive scoring work is
|
||||||
/// reused across calls at different tree levels.
|
/// reused across calls at different tree levels.
|
||||||
|
|
@ -262,7 +175,12 @@ pub fn match_forward(
|
||||||
cost_matrix.push(row);
|
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 {
|
for (row, col) in assignment {
|
||||||
let si = avail_snap[row];
|
let si = avail_snap[row];
|
||||||
let ti = avail_tree[col];
|
let ti = avail_tree[col];
|
||||||
|
|
@ -419,7 +337,12 @@ fn match_children_for_scoring(
|
||||||
cost_matrix.push(row);
|
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 {
|
for (row, col) in assignment {
|
||||||
let si = avail_snap[row];
|
let si = avail_snap[row];
|
||||||
let ti = avail_tree[col];
|
let ti = avail_tree[col];
|
||||||
|
|
|
||||||
|
|
@ -26,103 +26,6 @@ const UNMATCHED_PENALTY: u32 = 10_000;
|
||||||
/// only flat property comparison is used (no subtree recursion).
|
/// only flat property comparison is used (no subtree recursion).
|
||||||
const MAX_SCORING_DEPTH: u32 = 3;
|
const MAX_SCORING_DEPTH: u32 = 3;
|
||||||
|
|
||||||
/// Minimum-cost bipartite matching via the Hungarian (Kuhn-Munkres) algorithm.
|
|
||||||
///
|
|
||||||
/// Given a cost matrix `cost[i][j]` for `rows` row-agents and `cols`
|
|
||||||
/// column-agents, returns a Vec of `(row, col)` pairs representing the
|
|
||||||
/// optimal assignment that minimizes total cost. Handles rectangular
|
|
||||||
/// matrices by padding to square with `UNMATCHED_PENALTY`.
|
|
||||||
///
|
|
||||||
/// Complexity: O(N^3) where N = max(rows, cols).
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Build square matrix, padding with `big` for dummy entries.
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// u[i] = potential for row i, v[j] = potential for column j
|
|
||||||
let mut u = vec![0i64; n + 1];
|
|
||||||
let mut v = vec![0i64; n + 1];
|
|
||||||
// p[j] = row assigned to column j (1-indexed, 0 = unassigned)
|
|
||||||
let mut p = vec![0usize; n + 1];
|
|
||||||
// way[j] = previous column in augmenting path for column j
|
|
||||||
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 syncback matching algorithm.
|
/// Session-scoped cache for the syncback matching algorithm.
|
||||||
pub struct MatchingSession {
|
pub struct MatchingSession {
|
||||||
cost_cache: RefCell<HashMap<(Ref, Ref), u32>>,
|
cost_cache: RefCell<HashMap<(Ref, Ref), u32>>,
|
||||||
|
|
@ -295,7 +198,12 @@ pub fn match_children(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimal assignment via the Hungarian algorithm.
|
// Optimal assignment via the Hungarian algorithm.
|
||||||
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 {
|
for (row, col) in assignment {
|
||||||
let ni = avail_new[row];
|
let ni = avail_new[row];
|
||||||
let oi = avail_old[col];
|
let oi = avail_old[col];
|
||||||
|
|
@ -469,7 +377,12 @@ fn match_children_for_scoring(
|
||||||
cost_matrix.push(row);
|
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 {
|
for (row, col) in assignment {
|
||||||
let ni = avail_new[row];
|
let ni = avail_new[row];
|
||||||
let oi = avail_old[col];
|
let oi = avail_old[col];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue