munkres-ts
Version:
Munkres (aka Hungarian) algorithm for TS
341 lines • 9.25 kB
JavaScript
/**
* Calculates the Munkres (also known as the Hungarian algorithm) solution
* to the classical assignment problem.
*
* Compute the indices for the lowest-cost pairings between rows and columns.
* Returns a list of (row, column) tuples that can be used to traverse the matrix.
*
* @param cost_matrix the nxn cost matrix
*/
function munkres(cost_matrix) {
const C = structuredClone(cost_matrix);
const n = C.length;
const row_covered = new Array(n).fill(false);
const col_covered = new Array(n).fill(false);
const path = zeroMatrix(n * 2);
const marked = zeroMatrix(n);
const state = {
n, C, row_covered, col_covered, marked, path,
Z0_c: 0, Z0_r: 0
};
const steps = [
undefined, step1, step2, step3, step4, step5, step6
];
let step = 1; // step 7 means done
while (true) {
if (step === 7) {
break;
}
const func = steps[step];
step = func(state);
}
const results = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (marked[i][j] === 1) {
results.push(j);
}
}
}
return results;
}
;
/**
* For each row of the matrix, find the smallest element and
* subtract it from every element in its row.
*
* * modifies `C`
*/
function step1(state) {
const { n, C } = state;
for (let i = 0; i < n; i++) {
const minval = Math.min(...C[i]);
for (let j = 0; j < n; j++) {
C[i][j] -= minval;
}
}
return 2;
}
/**
* Find a zero (Z) in the resulting matrix. If there is no starred
* zero in its row or column, star Z. Repeat for each element in the
* matrix. Go to Step 3.
*
* * modifies `row_covered` and `col_covered`
*/
function step2(state) {
const { n, C, row_covered, col_covered, marked } = state;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (C[i][j] === 0 &&
!col_covered[j] &&
!row_covered[i]) {
marked[i][j] = 1;
col_covered[j] = true;
row_covered[i] = true;
break;
}
}
}
clear_covers(n, row_covered, col_covered);
return 3;
}
/**
* Cover each column containing a starred zero. If K columns are
* covered, the starred zeros describe a complete set of unique
* assignments. In this case, Go to DONE, otherwise, Go to Step 4.
*
* * modifies `col_covered`
*/
function step3(state) {
const { n, col_covered, marked } = state;
let count = 0;
for (let i = 0; i < n; ++i) {
for (let j = 0; j < n; ++j) {
if (marked[i][j] === 1 && !col_covered[j]) {
col_covered[j] = true;
count++;
}
}
}
return (count >= n) ? 7 : 4;
}
/**
* Find a noncovered zero and prime it. If there is no starred zero
* in the row containing this primed zero, Go to Step 5. Otherwise,
* cover this row and uncover the column containing the starred
* zero. Continue in this manner until there are no uncovered zeros
* left. Save the smallest uncovered value and Go to Step 6.
*
* * modifies `row_covered`, `col_covered`, `Z0_r` and `Z0_c`
*/
function step4(state) {
const { n, row_covered, col_covered, marked } = state;
let row = -1;
let col = -1;
let star_col = -1;
while (true) {
[row, col] = find_a_zero(state);
if (row < 0) {
return 6;
}
marked[row][col] = 2;
star_col = find_star_in_row(n, marked, row);
if (star_col >= 0) {
col = star_col;
row_covered[row] = true;
col_covered[col] = false;
}
else {
state.Z0_r = row;
state.Z0_c = col;
return 5;
}
}
}
;
/**
* Construct a series of alternating primed and starred zeros as
* follows. Let Z0 represent the uncovered primed zero found in Step 4.
* Let Z1 denote the starred zero in the column of Z0 (if any).
* Let Z2 denote the primed zero in the row of Z1 (there will always
* be one). Continue until the series terminates at a primed zero
* that has no starred zero in its column. Unstar each starred zero
* of the series, star each primed zero of the series, erase all
* primes and uncover every line in the matrix. Return to Step 3
*
* * modifies `path`, `row_covered`, `col_covered`
*/
function step5(state) {
const { n, row_covered, col_covered, marked, path } = state;
let count = 0;
path[count][0] = state.Z0_r;
path[count][1] = state.Z0_c;
let done = false;
while (!done) {
const row = find_star_in_col(n, marked, path[count][1]);
if (row >= 0) {
count++;
path[count][0] = row;
path[count][1] = path[count - 1][1];
}
else {
done = true;
}
if (!done) {
const col = find_prime_in_row(n, marked, path[count][0]);
count++;
path[count][0] = path[count - 1][0];
path[count][1] = col;
}
}
convert_path(marked, path, count);
clear_covers(n, row_covered, col_covered);
erase_primes(n, marked);
return 3;
}
;
/**
* Add the value found in Step 4 to every element of each covered
* row, and subtract it from every element of each uncovered column.
* Return to Step 4 without altering any stars, primes, or covered
* lines.
*
* * modifies `C`
*/
function step6(state) {
const { n, row_covered, col_covered, C } = state;
const minval = find_smallest(n, row_covered, col_covered, C);
for (let i = 0; i < n; ++i) {
for (let j = 0; j < n; ++j) {
if (row_covered[i])
C[i][j] += minval;
if (!col_covered[j])
C[i][j] -= minval;
}
}
return 4;
}
;
/**
* Returns the smallest uncovered value in the matrix,
* or
* if no value was found.
*/
function find_smallest(n, row_covered, col_covered, C) {
let minval = Number.POSITIVE_INFINITY;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (!row_covered[i] && !col_covered[j]) {
if (minval > C[i][j]) {
minval = C[i][j];
}
}
}
}
return minval;
}
;
/**
* Find the first uncovered element with value 0 and returns the indices
* of the found element or [-1, -1] if not found.
*/
function find_a_zero(state) {
const { n, row_covered, col_covered, C } = state;
for (let i = 0; i < n; ++i) {
for (let j = 0; j < n; ++j) {
if (C[i][j] === 0 &&
!row_covered[i] &&
!col_covered[j]) {
return [i, j];
}
}
}
return [-1, -1];
}
/**
* Find the first starred element in the specified row. Returns
* the column index, or -1 if no starred element was found.
*
* @param row the index of the row to search
*/
function find_star_in_row(n, marked, row) {
for (let j = 0; j < n; j++) {
if (marked[row][j] === 1) {
return j;
}
}
return -1;
}
;
/**
* Find the first starred element in the specified column and
* returns he row index, or -1 if no starred element was found.
*/
function find_star_in_col(n, marked, col) {
for (let i = 0; i < n; i++) {
if (marked[i][col] === 1) {
return i;
}
}
return -1;
}
;
/**
* Find the first prime element in the specified row and returns the
* column index, or -1 if no prime element was found.
*/
function find_prime_in_row(n, marked, row) {
for (let j = 0; j < n; j++) {
if (marked[row][j] === 2) {
return j;
}
}
return -1;
}
function convert_path(marked, path, count) {
for (let i = 0; i <= count; i++) {
marked[path[i][0]][path[i][1]] =
(marked[path[i][0]][path[i][1]] === 1) ? 0 : 1;
}
}
;
/**
* Erase all prime markings
*
* * modifies `marked`
*/
function erase_primes(n, marked) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (marked[i][j] === 2) {
marked[i][j] = 0;
}
}
}
}
;
/**
* Clear all covered matrix cells.
*
* * modifies `row_covered` and `col_covered`
*/
function clear_covers(n, row_covered, col_covered) {
for (let i = 0; i < n; i++) {
row_covered[i] = false;
col_covered[i] = false;
}
}
/**
* Returns an n×n zero matrix.
*
* @param n matrix dimensions
*/
function zeroMatrix(n) {
const M = [];
for (let i = 0; i < n; i++) {
let row = [];
for (let j = 0; j < n; j++) {
row.push(0);
}
M.push(row);
}
return M;
}
/**
* Calculates and returns the total cost based on the result of calling `munkres`.
*
* @param M the original matrix
* @param cols the result of calling `munkres`
*/
function calcTotalCost(M, cols) {
let total = 0;
for (let i = 0; i < cols.length; ++i) {
const col = cols[i];
const value = M[i][col];
total += value;
}
return total;
}
export { munkres, calcTotalCost };
//# sourceMappingURL=munkres.js.map