001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.math.combinatorics.optimisation; 031 032import java.util.Arrays; 033 034import Jama.Matrix; 035 036/* This code is adapted from Kevin L. Stern's implementation of the hungarian 037 * algorithm, available from: 038 * 039 * https://github.com/KevinStern/software-and-algorithms/blob/master/src/main/java/blogspot/software_and_algorithms/stern_library/optimization/HungarianAlgorithm.java 040 * 041 * The original copyright statement is as follows: 042 * 043 * Copyright (c) 2012 Kevin L. Stern 044 * 045 * Permission is hereby granted, free of charge, to any person obtaining a copy 046 * of this software and associated documentation files (the "Software"), to deal 047 * in the Software without restriction, including without limitation the rights 048 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 049 * copies of the Software, and to permit persons to whom the Software is 050 * furnished to do so, subject to the following conditions: 051 * 052 * The above copyright notice and this permission notice shall be included in 053 * all copies or substantial portions of the Software. 054 * 055 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 056 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 057 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 058 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 059 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 060 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 061 * SOFTWARE. 062 */ 063 064/** 065 * An implementation of the Hungarian algorithm for solving the assignment 066 * problem. An instance of the assignment problem consists of a number of 067 * workers along with a number of jobs and a cost matrix which gives the cost of 068 * assigning the i'th worker to the j'th job at position (i, j). The goal is to 069 * find an assignment of workers to jobs so that no job is assigned more than 070 * one worker and so that no worker is assigned to more than one job in such a 071 * manner so as to minimize the total cost of completing the jobs. 072 * <p> 073 * An assignment for a cost matrix that has more workers than jobs will 074 * necessarily include unassigned workers, indicated by an assignment value of 075 * -1; in no other circumstance will there be unassigned workers. Similarly, an 076 * assignment for a cost matrix that has more jobs than workers will necessarily 077 * include unassigned jobs; in no other circumstance will there be unassigned 078 * jobs. For completeness, an assignment for a square cost matrix will give 079 * exactly one unique worker to each job. 080 * <p> 081 * This version of the Hungarian algorithm runs in time O(n^3), where n is the 082 * maximum among the number of workers and the number of jobs. 083 * 084 * @author Kevin L. Stern 085 */ 086public class HungarianAlgorithm { 087 private final double[][] costMatrix; 088 private final int rows, cols, dim; 089 private final double[] labelByWorker, labelByJob; 090 private final int[] minSlackWorkerByJob; 091 private final double[] minSlackValueByJob; 092 private final int[] matchJobByWorker, matchWorkerByJob; 093 private final int[] parentWorkerByCommittedJob; 094 private final boolean[] committedWorkers; 095 096 /** 097 * Construct an instance of the algorithm. 098 * 099 * @param costMatrix 100 * the cost matrix, where matrix[i][j] holds the cost of 101 * assigning worker i to job j, for all i, j. The cost matrix 102 * must not be irregular in the sense that all rows must be the 103 * same length. 104 */ 105 public HungarianAlgorithm(Matrix costMatrix) { 106 this(costMatrix.getArray()); 107 } 108 109 /** 110 * Construct an instance of the algorithm. 111 * 112 * @param costMatrix 113 * the cost matrix, where matrix[i][j] holds the cost of 114 * assigning worker i to job j, for all i, j. The cost matrix 115 * must not be irregular in the sense that all rows must be the 116 * same length. 117 */ 118 public HungarianAlgorithm(double[][] costMatrix) { 119 this.dim = Math.max(costMatrix.length, costMatrix[0].length); 120 this.rows = costMatrix.length; 121 this.cols = costMatrix[0].length; 122 this.costMatrix = new double[this.dim][this.dim]; 123 for (int w = 0; w < this.dim; w++) { 124 if (w < costMatrix.length) { 125 if (costMatrix[w].length != this.cols) { 126 throw new IllegalArgumentException("Irregular cost matrix"); 127 } 128 this.costMatrix[w] = Arrays.copyOf(costMatrix[w], this.dim); 129 } else { 130 this.costMatrix[w] = new double[this.dim]; 131 } 132 } 133 labelByWorker = new double[this.dim]; 134 labelByJob = new double[this.dim]; 135 minSlackWorkerByJob = new int[this.dim]; 136 minSlackValueByJob = new double[this.dim]; 137 committedWorkers = new boolean[this.dim]; 138 parentWorkerByCommittedJob = new int[this.dim]; 139 matchJobByWorker = new int[this.dim]; 140 Arrays.fill(matchJobByWorker, -1); 141 matchWorkerByJob = new int[this.dim]; 142 Arrays.fill(matchWorkerByJob, -1); 143 } 144 145 /** 146 * Compute an initial feasible solution by assigning zero labels to the 147 * workers and by assigning to each job a label equal to the minimum cost 148 * among its incident edges. 149 */ 150 protected void computeInitialFeasibleSolution() { 151 for (int j = 0; j < dim; j++) { 152 labelByJob[j] = Double.POSITIVE_INFINITY; 153 } 154 for (int w = 0; w < dim; w++) { 155 for (int j = 0; j < dim; j++) { 156 if (costMatrix[w][j] < labelByJob[j]) { 157 labelByJob[j] = costMatrix[w][j]; 158 } 159 } 160 } 161 } 162 163 /** 164 * Execute the algorithm. 165 * 166 * @return the minimum cost matching of workers to jobs based upon the 167 * provided cost matrix. A matching value of -1 indicates that the 168 * corresponding worker is unassigned. 169 */ 170 public int[] execute() { 171 /* 172 * Heuristics to improve performance: Reduce rows and columns by their 173 * smallest element, compute an initial non-zero dual feasible solution 174 * and create a greedy matching from workers to jobs of the cost matrix. 175 */ 176 reduce(); 177 computeInitialFeasibleSolution(); 178 greedyMatch(); 179 180 int w = fetchUnmatchedWorker(); 181 while (w < dim) { 182 initializePhase(w); 183 executePhase(); 184 w = fetchUnmatchedWorker(); 185 } 186 final int[] result = Arrays.copyOf(matchJobByWorker, rows); 187 for (w = 0; w < result.length; w++) { 188 if (result[w] >= cols) { 189 result[w] = -1; 190 } 191 } 192 return result; 193 } 194 195 /** 196 * Execute a single phase of the algorithm. A phase of the Hungarian 197 * algorithm consists of building a set of committed workers and a set of 198 * committed jobs from a root unmatched worker by following alternating 199 * unmatched/matched zero-slack edges. If an unmatched job is encountered, 200 * then an augmenting path has been found and the matching is grown. If the 201 * connected zero-slack edges have been exhausted, the labels of committed 202 * workers are increased by the minimum slack among committed workers and 203 * non-committed jobs to create more zero-slack edges (the labels of 204 * committed jobs are simultaneously decreased by the same amount in order 205 * to maintain a feasible labeling). 206 * <p> 207 * 208 * The runtime of a single phase of the algorithm is O(n^2), where n is the 209 * dimension of the internal square cost matrix, since each edge is visited 210 * at most once and since increasing the labeling is accomplished in time 211 * O(n) by maintaining the minimum slack values among non-committed jobs. 212 * When a phase completes, the matching will have increased in size. 213 */ 214 protected void executePhase() { 215 while (true) { 216 int minSlackWorker = -1, minSlackJob = -1; 217 double minSlackValue = Double.POSITIVE_INFINITY; 218 for (int j = 0; j < dim; j++) { 219 if (parentWorkerByCommittedJob[j] == -1) { 220 if (minSlackValueByJob[j] < minSlackValue) { 221 minSlackValue = minSlackValueByJob[j]; 222 minSlackWorker = minSlackWorkerByJob[j]; 223 minSlackJob = j; 224 } 225 } 226 } 227 if (minSlackValue > 0) { 228 updateLabeling(minSlackValue); 229 } 230 parentWorkerByCommittedJob[minSlackJob] = minSlackWorker; 231 if (matchWorkerByJob[minSlackJob] == -1) { 232 /* 233 * An augmenting path has been found. 234 */ 235 int committedJob = minSlackJob; 236 int parentWorker = parentWorkerByCommittedJob[committedJob]; 237 while (true) { 238 final int temp = matchJobByWorker[parentWorker]; 239 match(parentWorker, committedJob); 240 committedJob = temp; 241 if (committedJob == -1) { 242 break; 243 } 244 parentWorker = parentWorkerByCommittedJob[committedJob]; 245 } 246 return; 247 } else { 248 /* 249 * Update slack values since we increased the size of the 250 * committed workers set. 251 */ 252 final int worker = matchWorkerByJob[minSlackJob]; 253 committedWorkers[worker] = true; 254 for (int j = 0; j < dim; j++) { 255 if (parentWorkerByCommittedJob[j] == -1) { 256 final double slack = costMatrix[worker][j] 257 - labelByWorker[worker] - labelByJob[j]; 258 if (minSlackValueByJob[j] > slack) { 259 minSlackValueByJob[j] = slack; 260 minSlackWorkerByJob[j] = worker; 261 } 262 } 263 } 264 } 265 } 266 } 267 268 /** 269 * 270 * @return the first unmatched worker or {@link #dim} if none. 271 */ 272 protected int fetchUnmatchedWorker() { 273 int w; 274 for (w = 0; w < dim; w++) { 275 if (matchJobByWorker[w] == -1) { 276 break; 277 } 278 } 279 return w; 280 } 281 282 /** 283 * Find a valid matching by greedily selecting among zero-cost matchings. 284 * This is a heuristic to jump-start the augmentation algorithm. 285 */ 286 protected void greedyMatch() { 287 for (int w = 0; w < dim; w++) { 288 for (int j = 0; j < dim; j++) { 289 if (matchJobByWorker[w] == -1 290 && matchWorkerByJob[j] == -1 291 && costMatrix[w][j] - labelByWorker[w] - labelByJob[j] == 0) 292 { 293 match(w, j); 294 } 295 } 296 } 297 } 298 299 /** 300 * Initialize the next phase of the algorithm by clearing the committed 301 * workers and jobs sets and by initializing the slack arrays to the values 302 * corresponding to the specified root worker. 303 * 304 * @param w 305 * the worker at which to root the next phase. 306 */ 307 protected void initializePhase(int w) { 308 Arrays.fill(committedWorkers, false); 309 Arrays.fill(parentWorkerByCommittedJob, -1); 310 committedWorkers[w] = true; 311 for (int j = 0; j < dim; j++) { 312 minSlackValueByJob[j] = costMatrix[w][j] - labelByWorker[w] 313 - labelByJob[j]; 314 minSlackWorkerByJob[j] = w; 315 } 316 } 317 318 /** 319 * Helper method to record a matching between worker w and job j. 320 */ 321 protected void match(int w, int j) { 322 matchJobByWorker[w] = j; 323 matchWorkerByJob[j] = w; 324 } 325 326 /** 327 * Reduce the cost matrix by subtracting the smallest element of each row 328 * from all elements of the row as well as the smallest element of each 329 * column from all elements of the column. Note that an optimal assignment 330 * for a reduced cost matrix is optimal for the original cost matrix. 331 */ 332 protected void reduce() { 333 for (int w = 0; w < dim; w++) { 334 double min = Double.POSITIVE_INFINITY; 335 for (int j = 0; j < dim; j++) { 336 if (costMatrix[w][j] < min) { 337 min = costMatrix[w][j]; 338 } 339 } 340 for (int j = 0; j < dim; j++) { 341 costMatrix[w][j] -= min; 342 } 343 } 344 final double[] min = new double[dim]; 345 for (int j = 0; j < dim; j++) { 346 min[j] = Double.POSITIVE_INFINITY; 347 } 348 for (int w = 0; w < dim; w++) { 349 for (int j = 0; j < dim; j++) { 350 if (costMatrix[w][j] < min[j]) { 351 min[j] = costMatrix[w][j]; 352 } 353 } 354 } 355 for (int w = 0; w < dim; w++) { 356 for (int j = 0; j < dim; j++) { 357 costMatrix[w][j] -= min[j]; 358 } 359 } 360 } 361 362 /** 363 * Update labels with the specified slack by adding the slack value for 364 * committed workers and by subtracting the slack value for committed jobs. 365 * In addition, update the minimum slack values appropriately. 366 */ 367 protected void updateLabeling(double slack) { 368 for (int w = 0; w < dim; w++) { 369 if (committedWorkers[w]) { 370 labelByWorker[w] += slack; 371 } 372 } 373 for (int j = 0; j < dim; j++) { 374 if (parentWorkerByCommittedJob[j] != -1) { 375 labelByJob[j] -= slack; 376 } else { 377 minSlackValueByJob[j] -= slack; 378 } 379 } 380 } 381}