├── README.md ├── lap.js └── thumbnail.png /README.md: -------------------------------------------------------------------------------- 1 | ## LAP-JV 2 | ### Linear Assignment Problem — algorithm by R. Jonker and A. Volgenant 3 | 4 | “A shortest augmenting path algorithm for dense and sparse linear assignment problems,” by R. Jonker and A. Volgenant, _Computing_ (1987) 38: 325. doi:10.1007/BF02278710 5 | 6 | Ported to javascript by Philippe Rivière, from the C++ implementation found at https://github.com/yongyanghz/LAPJV-algorithm-c 7 | 8 | Added an epsilon to avoid infinite loops caused by rounding errors. 9 | 10 | 11 | ## Usage 12 | 13 | In the [Linear Assignment Problem](https://en.wikipedia.org/wiki/Assignment_problem), you have _n_ agents and _n_ tasks, and need to assign one task to each agent, at minimal cost. 14 | 15 | First, compute the cost matrix: how expensive it is to assign agent _i_ (rows) to task _j_ (columns). 16 | 17 | The LAP-JV algorithm will give an optimal solution: 18 | 19 | ```javascript 20 | n = 3, costs = [[1,2,3], [4,2,1], [2,2,2]]; 21 |  //               ^ _ _    _ _ ^    _ ^ _ 22 | solution = lap(n, costs); 23 | 24 | console.log(solution.col); 25 | // [0, 2, 1] 26 | console.log(solution.cost); 27 | // 4 28 | ``` 29 | 30 | Here agent 0 is assigned to task 0, agent 1 to task 2, agent 2 to task 1, resulting in a total cost of `1 + 1 + 2 = 4`. 31 | 32 | 33 | **Cost callback** 34 | 35 | For performance and usability reasons, the `lap` function now accepts a cost callback `cost(i,j)` instead of a cost matrix: 36 | ```javascript 37 | var pos = new Float32Array(1000).map(d => Math.random() * 1000); 38 | lap(pos.length, (i,j) => (pos[i] - j) * (pos[i] - j)); 39 | ``` 40 | 41 | ## 42 | 43 | The algorithm runs in `O(n^2)`. You can run it [directly](http://bl.ocks.org/Fil/6ead5eea43ec506d5550f095edc45e3f) or as a javascript worker, as in the following example: 44 | 45 | [![](https://gist.githubusercontent.com/Fil/d9752d8c41cc2cc176096ce475233966/raw/88c1e7e4d62df8145a68808b7252cd5013e0394f/thumbnail.png)](https://observablehq.observablehq.cloud/pangea/varia/lap-jv) 46 | 47 | In the example above, we assign _n_ points to a grid of _n_ positions. `costs[i][j]` is the square distance between point _i_'s original coordinates and position _j_'s coordinates. The algorithm minimizes the total cost, i.e. the sum of square displacements. 48 | 49 | 50 | ## 51 | 52 | Comments and patches at [Fil/lap-jv](https://github.com/Fil/lap-jv). 53 | -------------------------------------------------------------------------------- /lap.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * 3 | * lap.js -- ported to javascript from 4 | 5 | lap.cpp 6 | version 1.0 - 4 September 1996 7 | author: Roy Jonker @ MagicLogic Optimization Inc. 8 | e-mail: roy_jonker@magiclogic.com 9 | 10 | Code for Linear Assignment Problem, according to 11 | 12 | "A Shortest Augmenting Path Algorithm for Dense and Sparse Linear 13 | Assignment Problems," Computing 38, 325-340, 1987 14 | 15 | by 16 | 17 | R. Jonker and A. Volgenant, University of Amsterdam. 18 | 19 | * 20 | PORTED TO JAVASCRIPT 2017-01-02 by Philippe Riviere(fil@rezo.net) 21 | CHANGED 2016-05-13 by Yang Yong(yangyongeducation@163.com) in column reduction part according to 22 | matlab version of LAPJV algorithm(Copyright (c) 2010, Yi Cao All rights reserved)-- 23 | https://www.mathworks.com/matlabcentral/fileexchange/26836-lapjv-jonker-volgenant-algorithm-for-linear-assignment-problem-v3-0: 24 | * 25 | *************************************************************************/ 26 | 27 | /* This function is the jv shortest augmenting path algorithm to solve the assignment problem */ 28 | function lap(dim, cost) { 29 | // input: 30 | // dim - problem size 31 | // cost - cost callback (or matrix) 32 | 33 | // output: 34 | // rowsol - column assigned to row in solution 35 | // colsol - row assigned to column in solution 36 | // u - dual variables, row reduction numbers 37 | // v - dual variables, column reduction numbers 38 | 39 | // convert the cost matrix (old API) to a callback (new API) 40 | if (typeof cost === "object") { 41 | var cost_matrix = cost; 42 | cost = function(i, j) { 43 | return cost_matrix[i][j]; 44 | }; 45 | } 46 | 47 | var sum = 0; 48 | { 49 | let i1, j1; 50 | for (i1 = 0; i1 < dim; i1++) { 51 | for (j1 = 0; j1 < dim; j1++) 52 | sum += cost(i1, j1); 53 | } 54 | } 55 | const BIG = 10000 * (sum / dim); 56 | const epsilon = sum / dim / 10000; 57 | const rowsol = new Int32Array(dim), 58 | colsol = new Int32Array(dim), 59 | u = new Float64Array(dim), 60 | v = new Float64Array(dim); 61 | let unassignedfound; 62 | /* row */ 63 | let i, imin, numfree = 0, prvnumfree, f, i0, k, freerow; // *pred, *free 64 | /* col */ 65 | let j, j1, j2, endofpath, last, low, up; // *collist, *matches 66 | /* cost */ 67 | let min, h, umin, usubmin, v2; // *d 68 | 69 | const free = new Int32Array(dim); // list of unassigned rows. 70 | const collist = new Int32Array(dim); // list of columns to be scanned in various ways. 71 | const matches = new Int32Array(dim); // counts how many times a row could be assigned. 72 | const d = new Float64Array(dim); // 'cost-distance' in augmenting path calculation. 73 | const pred = new Int32Array(dim); // row-predecessor of column in augmenting/alternating path. 74 | 75 | // init how many times a row will be assigned in the column reduction. 76 | for (i = 0; i < dim; i++) 77 | matches[i] = 0; 78 | 79 | // COLUMN REDUCTION 80 | for ( 81 | j = dim; 82 | j--; // reverse order gives better results. 83 | 84 | ) { 85 | // find minimum cost over rows. 86 | min = cost(0, j); 87 | imin = 0; 88 | for (i = 1; i < dim; i++) 89 | if (cost(i, j) < min) { 90 | min = cost(i, j); 91 | imin = i; 92 | } 93 | v[j] = min; 94 | if (++matches[imin] == 1) { 95 | // init assignment if minimum row assigned for first time. 96 | rowsol[imin] = j; 97 | colsol[j] = imin; 98 | } else if (v[j] < v[rowsol[imin]]) { 99 | j1 = rowsol[imin]; 100 | rowsol[imin] = j; 101 | colsol[j] = imin; 102 | colsol[j1] = -1; 103 | } else colsol[j] = -1; // row already assigned, column not assigned. 104 | } 105 | 106 | // REDUCTION TRANSFER 107 | for (i = 0; i < dim; i++) { 108 | if ( 109 | matches[i] == 0 // fill list of unassigned 'free' rows. 110 | ) 111 | free[numfree++] = i; 112 | else if (matches[i] == 1) { 113 | // transfer reduction from rows that are assigned once. 114 | j1 = rowsol[i]; 115 | min = BIG; 116 | for (j = 0; j < dim; j++) 117 | if (j != j1) 118 | if (cost(i, j) - v[j] < min + epsilon) min = cost(i, j) - v[j]; 119 | v[j1] = v[j1] - min; 120 | } 121 | } 122 | 123 | // AUGMENTING ROW REDUCTION 124 | let loopcnt = 0; // do-loop to be done twice. 125 | do { 126 | loopcnt++; 127 | 128 | // scan all free rows. 129 | // in some cases, a free row may be replaced with another one to be scanned next. 130 | k = 0; 131 | prvnumfree = numfree; 132 | numfree = 0; // start list of rows still free after augmenting row reduction. 133 | while (k < prvnumfree) { 134 | i = free[k]; 135 | k++; 136 | 137 | // find minimum and second minimum reduced cost over columns. 138 | umin = cost(i, 0) - v[0]; 139 | j1 = 0; 140 | usubmin = BIG; 141 | for (j = 1; j < dim; j++) { 142 | h = cost(i, j) - v[j]; 143 | if (h < usubmin) 144 | if (h >= umin) { 145 | usubmin = h; 146 | j2 = j; 147 | } else { 148 | usubmin = umin; 149 | umin = h; 150 | j2 = j1; 151 | j1 = j; 152 | } 153 | } 154 | 155 | i0 = colsol[j1]; 156 | if (umin < usubmin + epsilon) 157 | // change the reduction of the minimum column to increase the minimum 158 | // reduced cost in the row to the subminimum. 159 | v[j1] = v[j1] - (usubmin + epsilon - umin); 160 | else if (i0 > -1) { 161 | // minimum and subminimum equal. 162 | // minimum column j1 is assigned. 163 | // swap columns j1 and j2, as j2 may be unassigned. 164 | j1 = j2; 165 | i0 = colsol[j2]; 166 | } 167 | 168 | // (re-)assign i to j1, possibly de-assigning an i0. 169 | rowsol[i] = j1; 170 | colsol[j1] = i; 171 | 172 | if (i0 > -1) 173 | if (umin < usubmin) 174 | // minimum column j1 assigned earlier. 175 | // put in current k, and go back to that k. 176 | // continue augmenting path i - j1 with i0. 177 | free[--k] = i0; 178 | else 179 | // no further augmenting reduction possible. 180 | // store i0 in list of free rows for next phase. 181 | free[numfree++] = i0; 182 | } 183 | } while (loopcnt < 2); // repeat once. 184 | 185 | // AUGMENT SOLUTION for each free row. 186 | for (f = 0; f < numfree; f++) { 187 | freerow = free[f]; // start row of augmenting path. 188 | 189 | // Dijkstra shortest path algorithm. 190 | // runs until unassigned column added to shortest path tree. 191 | for (j = dim; j--; ) { 192 | d[j] = cost(freerow, j) - v[j]; 193 | pred[j] = freerow; 194 | collist[j] = j; // init column list. 195 | } 196 | 197 | low = 0; // columns in 0..low-1 are ready, now none. 198 | up = 0; // columns in low..up-1 are to be scanned for current minimum, now none. 199 | // columns in up..dim-1 are to be considered later to find new minimum, 200 | // at this stage the list simply contains all columns 201 | unassignedfound = false; 202 | do { 203 | if (up == low) { 204 | // no more columns to be scanned for current minimum. 205 | last = low - 1; 206 | 207 | // scan columns for up..dim-1 to find all indices for which new minimum occurs. 208 | // store these indices between low..up-1 (increasing up). 209 | min = d[collist[up++]]; 210 | for (k = up; k < dim; k++) { 211 | j = collist[k]; 212 | h = d[j]; 213 | if (h <= min) { 214 | if (h < min) { 215 | // new minimum. 216 | up = low; // restart list at index low. 217 | min = h; 218 | } 219 | // new index with same minimum, put on undex up, and extend list. 220 | collist[k] = collist[up]; 221 | collist[up++] = j; 222 | } 223 | } 224 | // check if any of the minimum columns happens to be unassigned. 225 | // if so, we have an augmenting path right away. 226 | for (k = low; k < up; k++) 227 | if (colsol[collist[k]] < 0) { 228 | endofpath = collist[k]; 229 | unassignedfound = true; 230 | break; 231 | } 232 | } 233 | 234 | if (!unassignedfound) { 235 | // update 'distances' between freerow and all unscanned columns, via next scanned column. 236 | j1 = collist[low]; 237 | low++; 238 | i = colsol[j1]; 239 | h = cost(i, j1) - v[j1] - min; 240 | 241 | for (k = up; k < dim; k++) { 242 | j = collist[k]; 243 | v2 = cost(i, j) - v[j] - h; 244 | if (v2 < d[j]) { 245 | pred[j] = i; 246 | if (v2 == min) 247 | if (colsol[j] < 0) { 248 | // new column found at same minimum value 249 | // if unassigned, shortest augmenting path is complete. 250 | endofpath = j; 251 | unassignedfound = true; 252 | break; 253 | } else { 254 | // else add to list to be scanned right away. 255 | collist[k] = collist[up]; 256 | collist[up++] = j; 257 | } 258 | d[j] = v2; 259 | } 260 | } 261 | } 262 | } while (!unassignedfound); 263 | 264 | // update column prices. 265 | for (k = last + 1; k--; ) { 266 | j1 = collist[k]; 267 | v[j1] = v[j1] + d[j1] - min; 268 | } 269 | 270 | // reset row and column assignments along the alternating path. 271 | do { 272 | i = pred[endofpath]; 273 | colsol[endofpath] = i; 274 | j1 = endofpath; 275 | endofpath = rowsol[i]; 276 | rowsol[i] = j1; 277 | } while (i != freerow); 278 | } 279 | 280 | // calculate optimal cost. 281 | let lapcost = 0; 282 | for (i = dim; i--; ) { 283 | j = rowsol[i]; 284 | u[i] = cost(i, j) - v[j]; 285 | lapcost = lapcost + cost(i, j); 286 | } 287 | 288 | return { 289 | cost: lapcost, 290 | row: rowsol, 291 | col: colsol, 292 | u: u, 293 | v: v 294 | }; 295 | } 296 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fil/lap-jv/0e5dfea61149f6a4b2ff094484469e31cf48c797/thumbnail.png --------------------------------------------------------------------------------