1 module nxt.hungarian; 2 3 version (none): /+ TODO: activate +/ 4 5 /// Origin: http://fantascienza.net/leonardo/so/hungarian.d. 6 7 /* Copyright (c) 2012 Kevin L. Stern 8 * 9 * Permission is hereby granted, free of charge, to any person obtaining a copy 10 * of this software and associated documentation files (the "Software"), to deal 11 * in the Software without restriction, including without limitation the rights 12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 * copies of the Software, and to permit persons to whom the Software is 14 * furnished to do so, subject to the following conditions: 15 * 16 * The above copyright notice and this permission notice shall be included in 17 * all copies or substantial portions of the Software. 18 * 19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 * SOFTWARE. 26 */ 27 28 /** 29 * An implementation of the Hungarian algorithm for solving the assignment 30 * problem. An instance of the assignment problem consists of a number of 31 * workers along with a number of jobs and a cost matrix which gives the cost of 32 * assigning the i'th worker to the j'th job at position (i, j). The goal is to 33 * find an assignment of workers to jobs so that no job is assigned more than 34 * one worker and so that no worker is assigned to more than one job in such a 35 * manner so as to minimize the total cost of completing the jobs. 36 * <p> 37 * 38 * An assignment for a cost matrix that has more workers than jobs will 39 * necessarily include unassigned workers, indicated by an assignment value of 40 * -1; in no other circumstance will there be unassigned workers. Similarly, an 41 * assignment for a cost matrix that has more jobs than workers will necessarily 42 * include unassigned jobs; in no other circumstance will there be unassigned 43 * jobs. For completeness, an assignment for a square cost matrix will give 44 * exactly one unique worker to each job. 45 * 46 * This version of the Hungarian algorithm runs in time O(n^3), where n is the 47 * maximum among the number of workers and the number of jobs. 48 * 49 * @author Kevin L. Stern 50 * 51 * Ported to D language, Oct 8 2012 V.1.2, by leonardo maffi. 52 */ 53 struct HungarianAlgorithm(T) { 54 import std.algorithm: max; 55 import std.traits: isFloatingPoint; 56 57 static if (isFloatingPoint!T) 58 enum T infinity = T.infinity; 59 else 60 enum T infinity = T.max; // Doesn't work with Bigint. 61 62 private T[][] costMatrix; 63 private int rows, cols, dim; 64 private T[] labelByWorker, labelByJob, minSlackValueByJob; 65 private int[] minSlackWorkerByJob, matchJobByWorker, 66 matchWorkerByJob, parentWorkerByCommittedJob; 67 private bool[] committedWorkers; 68 69 /** 70 * Construct an instance of the algorithm and Execute the algorithm. 71 * 72 * @param costMatrix 73 * the cost matrix, where matrix[i][j] holds the cost of 74 * assigning worker i to job j, for all i, j. The cost matrix 75 * must not be irregular in the sense that all rows must be the 76 * same length. 77 * 78 * @return the minimum cost matching of workers to jobs based upon the 79 * provided cost matrix. A matching value of -1 indicates that the 80 * corresponding worker is unassigned. 81 */ 82 public static int[] opCall(in T[][] costMatrix_) 83 pure /*nothrow*/ 84 in(costMatrix_.length > 0) 85 in 86 { 87 foreach (row; costMatrix_) // is rectangular 88 assert(row.length == costMatrix_[0].length); 89 } 90 do 91 { 92 HungarianAlgorithm self; 93 self.dim = max(costMatrix_.length, costMatrix_[0].length); 94 self.rows = costMatrix_.length; 95 self.cols = costMatrix_[0].length; 96 self.costMatrix = new T[][](self.dim, self.dim); 97 98 foreach (w; 0 .. self.dim) 99 if (w < costMatrix_.length) 100 self.costMatrix[w] = copyOf(costMatrix_[w], self.dim); 101 else 102 self.costMatrix[w][] = 0; // For Java semantics. 103 104 self.labelByWorker.length = self.dim; 105 self.labelByWorker[] = 0; // Necessary to follow Java semantics. 106 self.labelByJob.length = self.dim; 107 self.minSlackWorkerByJob.length = self.dim; 108 self.minSlackValueByJob.length = self.dim; 109 self.committedWorkers.length = self.dim; 110 self.parentWorkerByCommittedJob.length = self.dim; 111 self.matchJobByWorker.length = self.dim; 112 self.matchJobByWorker[] = -1; 113 self.matchWorkerByJob.length = self.dim; 114 self.matchWorkerByJob[] = -1; 115 116 /* 117 * Heuristics to improve performance: Reduce rows and columns by their 118 * smallest element, compute an initial non-zero dual feasible solution 119 * and create a greedy matching from workers to jobs of the cost matrix. 120 */ 121 self.reduce(); 122 self.computeInitialFeasibleSolution(); 123 self.greedyMatch(); 124 125 auto w = self.fetchUnmatchedWorker(); 126 while (w < self.dim) { 127 self.initializePhase(w); 128 self.executePhase(); 129 w = self.fetchUnmatchedWorker(); 130 } 131 auto result = copyOf(self.matchJobByWorker, self.rows); 132 foreach (ref r; result) 133 if (r >= self.cols) 134 r = -1; 135 return result; 136 } 137 138 static T[] copyOf(T)(in T[] input, in size_t size) pure /*nothrow*/ { 139 if (size <= input.length) 140 return input[0 .. size].dup; 141 else { 142 auto result = new T[size]; 143 result[0 .. input.length] = input[]; 144 result[input.length .. $] = 0; // Necessary to follow Java semantics. 145 return result; 146 } 147 } 148 149 /** 150 * Compute an initial feasible solution by assigning zero labels to the 151 * workers and by assigning to each job a label equal to the minimum cost 152 * among its incident edges. 153 */ 154 void computeInitialFeasibleSolution() pure nothrow { 155 labelByJob[] = infinity; 156 foreach (w, row; costMatrix) 157 foreach (j, rj; row) 158 if (rj < labelByJob[j]) 159 labelByJob[j] = rj; 160 } 161 162 /** 163 * Execute a single phase of the algorithm. A phase of the Hungarian 164 * algorithm consists of building a set of committed workers and a set of 165 * committed jobs from a root unmatched worker by following alternating 166 * unmatched/matched zero-slack edges. If an unmatched job is encountered, 167 * then an augmenting path has been found and the matching is grown. If the 168 * connected zero-slack edges have been exhausted, the labels of committed 169 * workers are increased by the minimum slack among committed workers and 170 * non-committed jobs to create more zero-slack edges (the labels of 171 * committed jobs are simultaneously decreased by the same amount in order 172 * to maintain a feasible labeling). 173 * 174 * The runtime of a single phase of the algorithm is O(n^2), where n is the 175 * dimension of the internal square cost matrix, since each edge is visited 176 * at most once and since increasing the labeling is accomplished in time 177 * O(n) by maintaining the minimum slack values among non-committed jobs. 178 * When a phase completes, the matching will have increased in size. 179 */ 180 void executePhase() pure nothrow { 181 while (true) { 182 int minSlackWorker = -1, minSlackJob = -1; 183 auto minSlackValue = infinity; 184 foreach (j, pj; parentWorkerByCommittedJob) 185 if (pj == -1) 186 if (minSlackValueByJob[j] < minSlackValue) { 187 minSlackValue = minSlackValueByJob[j]; 188 minSlackWorker = minSlackWorkerByJob[j]; 189 minSlackJob = j; 190 } 191 192 if (minSlackValue > 0) 193 updateLabeling(minSlackValue); 194 parentWorkerByCommittedJob[minSlackJob] = minSlackWorker; 195 if (matchWorkerByJob[minSlackJob] == -1) { 196 // An augmenting path has been found. 197 int committedJob = minSlackJob; 198 int parentWorker = parentWorkerByCommittedJob[committedJob]; 199 while (true) { 200 immutable temp = matchJobByWorker[parentWorker]; 201 match(parentWorker, committedJob); 202 committedJob = temp; 203 if (committedJob == -1) 204 break; 205 parentWorker = parentWorkerByCommittedJob[committedJob]; 206 } 207 return; 208 } else { 209 // Update slack values since we increased the size of the 210 // committed workers set. 211 immutable int worker = matchWorkerByJob[minSlackJob]; 212 committedWorkers[worker] = true; 213 for (int j = 0; j < dim; j++) { // Performance-critical. 214 if (parentWorkerByCommittedJob[j] == -1) { 215 immutable slack = cast(T)(costMatrix[worker][j] - 216 labelByWorker[worker] - 217 labelByJob[j]); 218 if (minSlackValueByJob[j] > slack) { 219 minSlackValueByJob[j] = slack; 220 minSlackWorkerByJob[j] = worker; 221 } 222 } 223 } 224 } 225 } 226 } 227 228 /** 229 * 230 * @return the first unmatched worker or {@link #dim} if none. 231 */ 232 int fetchUnmatchedWorker() const pure nothrow { 233 foreach (w, mw; matchJobByWorker) 234 if (mw == -1) 235 return w; 236 return dim; 237 } 238 239 /** 240 * Find a valid matching by greedily selecting among zero-cost matchings. 241 * This is a heuristic to jump-start the augmentation algorithm. 242 */ 243 void greedyMatch() pure nothrow { 244 foreach (w, row; costMatrix) 245 foreach (j, rj; row) 246 if (matchJobByWorker[w] == -1 247 && matchWorkerByJob[j] == -1 248 && rj - labelByWorker[w] - labelByJob[j] == 0) 249 match(w, j); 250 } 251 252 /** 253 * Initialize the next phase of the algorithm by clearing the committed 254 * workers and jobs sets and by initializing the slack arrays to the values 255 * corresponding to the specified root worker. 256 * 257 * @param w 258 * the worker at which to root the next phase. 259 */ 260 void initializePhase(in int w) pure nothrow { 261 committedWorkers[] = false; 262 parentWorkerByCommittedJob[] = -1; 263 committedWorkers[w] = true; 264 foreach (j, ref mj; minSlackValueByJob) { 265 mj = cast(T)(costMatrix[w][j] - labelByWorker[w] - labelByJob[j]); 266 minSlackWorkerByJob[j] = w; 267 } 268 } 269 270 /** 271 * Helper method to record a matching between worker w and job j. 272 */ 273 void match(in int w, in int j) pure nothrow { 274 matchJobByWorker[w] = j; 275 matchWorkerByJob[j] = w; 276 } 277 278 /** 279 * Reduce the cost matrix by subtracting the smallest element of each row 280 * from all elements of the row as well as the smallest element of each 281 * column from all elements of the column. Note that an optimal assignment 282 * for a reduced cost matrix is optimal for the original cost matrix. 283 */ 284 void reduce() pure /*nothrow*/ { 285 foreach (ref row; costMatrix) { 286 auto min = infinity; 287 foreach (r; row) 288 if (r < min) 289 min = r; 290 row[] -= min; 291 } 292 auto min = new T[dim]; 293 min[] = infinity; 294 foreach (row; costMatrix) 295 foreach (j, rj; row) 296 if (rj < min[j]) 297 min[j] = rj; 298 foreach (row; costMatrix) 299 row[] -= min[]; 300 } 301 302 /** 303 * Update labels with the specified slack by adding the slack value for 304 * committed workers and by subtracting the slack value for committed jobs. 305 * In addition, update the minimum slack values appropriately. 306 */ 307 void updateLabeling(in T slack) pure nothrow { 308 foreach (w, cw; committedWorkers) 309 if (cw) 310 labelByWorker[w] += slack; 311 foreach (j, pj; parentWorkerByCommittedJob) 312 if (pj != -1) 313 labelByJob[j] -= slack; 314 else 315 minSlackValueByJob[j] -= slack; 316 } 317 } 318 319 320 //void main() { 321 unittest { 322 import std.stdio, std.random, std.datetime, std.exception, core.exception, 323 std.typecons, std.range, std.algorithm, std.typetuple; 324 325 // { 326 // version (assert) { 327 // const double[][] mat0 = []; 328 // assertThrown!(AssertError)(HungarianAlgorithm!double(mat0)); 329 // } 330 // } 331 332 { 333 const double[][] mat0 = [[], []]; 334 const r0 = HungarianAlgorithm!double(mat0); 335 assert(r0 == [-1, -1]); 336 } 337 338 { 339 const double[][] mat0 = [[1]]; 340 const r0 = HungarianAlgorithm!double(mat0); 341 assert(r0 == [0]); 342 } 343 344 { 345 const double[][] mat0 = [[1], [1]]; 346 const r0 = HungarianAlgorithm!double(mat0); 347 assert(r0 == [0, -1]); 348 } 349 350 { 351 const double[][] mat0 = [[1, 1]]; 352 const r0 = HungarianAlgorithm!double(mat0); 353 assert(r0 == [0]); 354 } 355 356 { 357 const double[][] mat0 = [[1, 1], [1, 1]]; 358 const r0 = HungarianAlgorithm!double(mat0); 359 assert(r0 == [0, 1]); 360 } 361 362 { 363 const double[][] mat0 = [[1, 1], [1, 1], [1, 1]]; 364 const r0 = HungarianAlgorithm!double(mat0); 365 assert(r0 == [0, 1, -1]); 366 } 367 368 369 { 370 const double[][] mat0 = [[1, 2, 3], [6, 5, 4]]; 371 const r0 = HungarianAlgorithm!double(mat0); 372 assert(r0 == [0, 2]); 373 } 374 375 { 376 const double[][] mat0 = [[1, 2, 3], [6, 5, 4], [1, 1, 1]]; 377 const r0 = HungarianAlgorithm!double(mat0); 378 assert(r0 == [0, 2, 1]); 379 } 380 381 382 { 383 const int[][] mat0 = [[], []]; 384 const r0 = HungarianAlgorithm!int(mat0); 385 assert(r0 == [-1, -1]); 386 } 387 388 { 389 const int[][] mat0 = [[1]]; 390 const r0 = HungarianAlgorithm!int(mat0); 391 assert(r0 == [0]); 392 } 393 394 { 395 const int[][] mat0 = [[1], [1]]; 396 const r0 = HungarianAlgorithm!int(mat0); 397 assert(r0 == [0, -1]); 398 } 399 400 { 401 const int[][] mat0 = [[1, 1]]; 402 const r0 = HungarianAlgorithm!int(mat0); 403 assert(r0 == [0]); 404 } 405 406 { 407 const int[][] mat0 = [[1, 1], [1, 1]]; 408 const r0 = HungarianAlgorithm!int(mat0); 409 assert(r0 == [0, 1]); 410 } 411 412 { 413 const int[][] mat0 = [[1, 1], [1, 1], [1, 1]]; 414 const r0 = HungarianAlgorithm!int(mat0); 415 assert(r0 == [0, 1, -1]); 416 } 417 418 419 { 420 const int[][] mat0 = [[1, 2, 3], [6, 5, 4]]; 421 const r0 = HungarianAlgorithm!int(mat0); 422 assert(r0 == [0, 2]); 423 } 424 425 { 426 const int[][] mat0 = [[1, 2, 3], [6, 5, 4], [1, 1, 1]]; 427 const r0 = HungarianAlgorithm!int(mat0); 428 assert(r0 == [0, 2, 1]); 429 } 430 431 { 432 int[][] mat1 = [[ 7, 53, 183, 439, 863], 433 [497, 383, 563, 79, 973], 434 [287, 63, 343, 169, 583], 435 [627, 343, 773, 959, 943], 436 [767, 473, 103, 699, 303]]; 437 foreach (row; mat1) 438 row[] *= -1; 439 const r1 = HungarianAlgorithm!int(mat1); 440 // Currently doesn't work with -inline: 441 //r1.length.iota().map!(r => -mat1[r][r1[r]])().reduce!q{a + b}().writeln(); 442 int tot1 = 0; 443 foreach (i, r1i; r1) 444 tot1 -= mat1[i][r1i]; 445 assert(tot1 == 3315); 446 } 447 448 { 449 // Euler Problem 345: 450 // http://projecteuler.net/index.php?section=problems&id=345 451 int [][] mat2 = [ 452 [ 7, 53, 183, 439, 863, 497, 383, 563, 79, 973, 287, 63, 343, 169, 583], 453 [627, 343, 773, 959, 943, 767, 473, 103, 699, 303, 957, 703, 583, 639, 913], 454 [447, 283, 463, 29, 23, 487, 463, 993, 119, 883, 327, 493, 423, 159, 743], 455 [217, 623, 3, 399, 853, 407, 103, 983, 89, 463, 290, 516, 212, 462, 350], 456 [960, 376, 682, 962, 300, 780, 486, 502, 912, 800, 250, 346, 172, 812, 350], 457 [870, 456, 192, 162, 593, 473, 915, 45, 989, 873, 823, 965, 425, 329, 803], 458 [973, 965, 905, 919, 133, 673, 665, 235, 509, 613, 673, 815, 165, 992, 326], 459 [322, 148, 972, 962, 286, 255, 941, 541, 265, 323, 925, 281, 601, 95, 973], 460 [445, 721, 11, 525, 473, 65, 511, 164, 138, 672, 18, 428, 154, 448, 848], 461 [414, 456, 310, 312, 798, 104, 566, 520, 302, 248, 694, 976, 430, 392, 198], 462 [184, 829, 373, 181, 631, 101, 969, 613, 840, 740, 778, 458, 284, 760, 390], 463 [821, 461, 843, 513, 17, 901, 711, 993, 293, 157, 274, 94, 192, 156, 574], 464 [ 34, 124, 4, 878, 450, 476, 712, 914, 838, 669, 875, 299, 823, 329, 699], 465 [815, 559, 813, 459, 522, 788, 168, 586, 966, 232, 308, 833, 251, 631, 107], 466 [813, 883, 451, 509, 615, 77, 281, 613, 459, 205, 380, 274, 302, 35, 805]]; 467 foreach (row; mat2) 468 row[] *= -1; 469 const r2 = HungarianAlgorithm!int(mat2); 470 int tot2 = 0; 471 foreach (i, r2i; r2) 472 tot2 -= mat2[i][r2i]; 473 assert(tot2 == 13938); // Euler Problem 345 solution. 474 } 475 476 static T[][] genMat(T)(in int N, in int M, in int seed) { 477 rndGen.seed(seed); 478 auto mat = new T[][](N, M); 479 foreach (row; mat) 480 foreach (ref x; row) 481 static if (is(T == short)) 482 x = uniform(cast(T)10, cast(T)500); 483 else 484 x = uniform(cast(T)10, cast(T)5_000); 485 return mat; 486 } 487 488 // Steinhaus-Johnson-Trotter permutations algorithm 489 static struct Spermutations { 490 const int n; 491 alias Tuple!(int[], int) TResult; 492 493 int opApply(int delegate(ref TResult) dg) { 494 int result; 495 TResult aux; 496 497 int sign = 1; 498 alias Tuple!(int, int) Pair; 499 //auto p = iota(n).map!(i => Pair(i, i ? -1 : 0))().array(); 500 auto p = array(map!(i => Pair(i, i ? -1 : 0))(iota(n))); 501 502 aux = tuple(array(map!(pp => pp[0])(p)), sign); 503 result = dg(aux); if (result) goto END; 504 505 while (canFind!(pp => pp[1])(p)) { 506 // Failed using std.algorithm here, too much complex 507 auto largest = Pair(-100, -100); 508 int i1 = -1; 509 foreach (i, pp; p) 510 if (pp[1]) { 511 if (pp[0] > largest[0]) { 512 i1 = i; 513 largest = pp; 514 } 515 } 516 int n1 = largest[0], d1 = largest[1]; 517 518 sign *= -1; 519 int i2; 520 if (d1 == -1) { 521 i2 = i1 - 1; 522 swap(p[i1], p[i2]); 523 if (i2 == 0 || p[i2 - 1][0] > n1) 524 p[i2][1] = 0; 525 } else if (d1 == 1) { 526 i2 = i1 + 1; 527 swap(p[i1], p[i2]); 528 if (i2 == n - 1 || p[i2 + 1][0] > n1) 529 p[i2][1] = 0; 530 } 531 aux = tuple(array(map!(pp => pp[0])(p)), sign); 532 result = dg(aux); if (result) goto END; 533 534 foreach (i3, ref pp; p) { 535 auto n3 = pp[0], d3 = pp[1]; 536 if (n3 > n1) 537 pp[1] = (i3 < i2) ? 1 : -1; 538 } 539 } 540 541 END: return result; 542 } 543 } 544 545 static T bruteForceSolver(T)(in T[][] mat) /*pure nothrow*/ 546 in(mat.length > 0) 547 in 548 { 549 // Currently works only with square matrices. 550 foreach (row; mat) // Is square. 551 assert(row.length == mat.length); 552 } 553 do 554 { 555 auto maxTotal = -T.max; 556 foreach (p; Spermutations(mat.length)) { 557 T total = 0; 558 foreach (i, pi; p[0]) 559 total += mat[i][pi]; 560 maxTotal = max(maxTotal, total); 561 } 562 return maxTotal; 563 } 564 565 foreach (T; TypeTuple!(double, int, float, short, long)) { 566 // Fuzzy test. 567 foreach (test; 0 .. 10) { 568 auto mat3 = genMat!T(2 + test % 8, 2 + test % 8, test); 569 foreach (row; mat3) 570 row[] *= -1; 571 const r3 = HungarianAlgorithm!T(mat3); 572 T tot3 = 0; 573 foreach (i, r3i; r3) 574 tot3 -= mat3[i][r3i]; 575 foreach (row; mat3) 576 row[] *= -1; 577 assert(tot3 == bruteForceSolver(mat3)); 578 } 579 } 580 581 version (hungarian_benchmark) { 582 foreach (T; TypeTuple!(double, int, float, short, long)) { 583 auto mat4 = genMat!T(2_000, 2_000, 0); 584 StopWatch sw; 585 sw.start(); 586 const r4 = HungarianAlgorithm!T(mat4); 587 sw.stop(); 588 writefln("Type %s, milliseconds: %d", 589 T.stringof, sw.peek().msecs); 590 } 591 /* 592 Type double, milliseconds: 1032 593 Type int, milliseconds: 834 594 Type float, milliseconds: 859 595 Type short, milliseconds: 9221 596 Type long, milliseconds: 1306 597 */ 598 } 599 } 600 601 //void main() {}