├── speed_comparison ├── speed1.mat ├── speed1.pdf ├── speed1.png ├── speed2.mat ├── speed2.pdf ├── speed2.png ├── check_existence_lapjv_munkres.m ├── speed_makegraph.m └── speed_comparison.m ├── README.md ├── license.txt └── linear_sum_assignment.m /speed_comparison/speed1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed1.mat -------------------------------------------------------------------------------- /speed_comparison/speed1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed1.pdf -------------------------------------------------------------------------------- /speed_comparison/speed1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed1.png -------------------------------------------------------------------------------- /speed_comparison/speed2.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed2.mat -------------------------------------------------------------------------------- /speed_comparison/speed2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed2.pdf -------------------------------------------------------------------------------- /speed_comparison/speed2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrejdee/hungarian/HEAD/speed_comparison/speed2.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hungarian 2 | Hungarian algorithm implementation for linear sum assignment problem. Works for square and rectangular cost matrices. 3 | 4 | The speed of this implementation (referred to as LSA) is compared to two other ones: 5 | 6 | * [munkres](https://www.mathworks.com/matlabcentral/fileexchange/20652-hungarian-algorithm-for-linear-assignment-problems--v2-3-) 7 | * [lapjv](https://www.mathworks.com/matlabcentral/fileexchange/26836-lapjv-jonker-volgenant-algorithm-for-linear-assignment-problem-v3-0) 8 | 9 | For the problems tested, the LSA implementation is consistently much faster for rectangular problems (cost matrix M-by-N, M<N). 10 | 11 | ### Experiment 1 12 | 13 | M-by-N cost matrix obtained as the Euclidean distance between two sets of random points in (2D) plane: 14 | * ```u = rand(2, M);``` 15 | * ```v = .1*rand(2, N) + .5``` 16 | 17 | ![experiment 1](./speed_comparison/speed1.png "Experiment 1") 18 | 19 | ### Experiment 2 20 | 21 | M-by-N cost matrix is simply obtained as: ```rand(M, N)```. 22 | 23 | ![experiment 2](./speed_comparison/speed2.png "Experiment 2") 24 | 25 | 26 | -------------------------------------------------------------------------------- /speed_comparison/check_existence_lapjv_munkres.m: -------------------------------------------------------------------------------- 1 | function check_existence_lapjv_munkres() 2 | 3 | mfile_code = 2; 4 | 5 | if exist('lapjv') ~= mfile_code 6 | fprintf(['############################################################\n',... 7 | 'To reproduce these results, please download LAPJV from\n', ... 8 | 'the matlab file exchange, \n', ... 9 | 'https://www.mathworks.com/matlabcentral/fileexchange/26836-lapjv-jonker-volgenant-algorithm-for-linear-assignment-problem-v3-0\n', ... 10 | '############################################################\n']); 11 | error('lapjv not found'); 12 | end 13 | 14 | if exist('munkres') ~= mfile_code 15 | fprintf(['############################################################\n', 'To reproduce these results, please download MUNKRES from\n', ... 16 | 'the matlab file exchange, \n', ... 17 | 'https://www.mathworks.com/matlabcentral/fileexchange/20652-hungarian-algorithm-for-linear-assignment-problems--v2-3-\n', ... 18 | '############################################################\n']); 19 | error('munkres not found'); 20 | end 21 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Brian M. Clapper , Gael Varoquaux 2 | Copyright (c) 2016 Ondrej Drbohlav 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /speed_comparison/speed_makegraph.m: -------------------------------------------------------------------------------- 1 | function [functions, Ms, Ns, timings2] = speed_makegraph() 2 | 3 | load speed1 4 | timings = timings1; 5 | Ms = sizes(:, 1); 6 | Ns = sizes(:, 2); 7 | make_graph(Ms, Ns, functions, timings) 8 | 9 | load speed2 10 | timings = timings2; 11 | make_graph(Ms, Ns, functions, timings) 12 | 13 | function make_graph(Ms, Ns, functions, timings) 14 | figure('Position', [100, 100, 1500, 300]) 15 | hold on 16 | fN = length(functions); 17 | colors = {'r', 'g', 'b'}; 18 | 19 | % extract min and max of mean timings: 20 | mn_t = Inf; 21 | mx_t = 0; 22 | for k = 1:length(Ms) 23 | for l=1:fN 24 | t = timings{k, l}; 25 | mn_t = min( mean(t), mn_t); 26 | mx_t = max( mean(t), mx_t); 27 | end 28 | end 29 | 30 | for k = 1:length(Ms) 31 | M = Ms(k); 32 | N = Ns(k); 33 | h = zeros(size(functions)); 34 | for l=1:fN 35 | t = timings{k, l}; 36 | mn = min(t) ; 37 | mx = max(t) ; 38 | mean_ = mean(t); 39 | 40 | x = k+(l-1)*.1; 41 | %h(l) = errorbar(x, log10(mean_), log10(mean_)-log10(mn), log10(mx)-log10(mean_), 'color', colors{l}) 42 | % display just mean to avoid clutter: 43 | h(l)=plot(x, log10(mean_), 'x', 'color', colors{l}, ... 44 | 'markersize', 14, 'linew', 2); 45 | end 46 | 47 | % plot delimiters 48 | plot([k+.5, k+.5], [log10(mn_t), log10(mx_t)], 'k--'); 49 | end 50 | % plot delimiter before 1st item: 51 | plot([0+.5, 0+.5], [log10(mn_t), log10(mx_t)], 'k--'); 52 | 53 | legend(h, 'LSA', 'munkres', 'lapjv', 'location', 'northwest') 54 | ax = gca; 55 | N = length(Ms); 56 | set(ax, 'xtick', 1:N); 57 | % generate labels: 58 | labels = cell(1,N); 59 | for k=1:N 60 | labels{k} = sprintf('%ix%i', Ms(k), Ns(k)); 61 | end 62 | set(ax, 'xticklab', labels); 63 | 64 | ylabel('log10(time in seconds)') 65 | xlim([-1, N+1]) 66 | -------------------------------------------------------------------------------- /speed_comparison/speed_comparison.m: -------------------------------------------------------------------------------- 1 | function [functions, Ms, Ns, timings2] = speed_comparison() 2 | 3 | check_existence_lapjv_munkres(); 4 | 5 | rand('seed', 1) % set seed for repeatable results 6 | [functions, sizes, timings1] = speed_comparison_(30000, @random1); 7 | save speed1 functions sizes timings1 8 | 9 | rand('seed', 1) 10 | [functions, sizes, timings2] = speed_comparison_(30000, @random2); 11 | save speed2 functions sizes timings2 12 | 13 | function cost_matrix = random1_(dim, M, N, sc, shift) 14 | % distance between two sets of random points 15 | % dim = dimensionality 16 | % M, N: cardinality of the two sets 17 | % sc, shift: see code 18 | u = rand(dim, M); 19 | v = sc*rand(dim, N) + shift; 20 | D = sqrt(dist2(u, v)); 21 | cost_matrix = D; 22 | 23 | function cost_matrix = random1(M, N) 24 | cost_matrix = random1_(2, M, N, .1, 0.5); 25 | 26 | function cost_matrix = random2(M, N) 27 | cost_matrix = rand(M, N); 28 | 29 | function [functions, sizes, timings] = speed_comparison_(repC, data_fn) 30 | 31 | functions = {@linear_sum_assignment, @munkres, @lapjv} 32 | 33 | % size of problems: 34 | sizes = [[10, 10], 35 | [20, 20], 36 | [50, 50], 37 | [100, 100], 38 | [10, 30], 39 | [20, 60], 40 | [50, 150], 41 | [100, 300], 42 | [10, 50], 43 | [20, 100], 44 | [50, 250], 45 | [100, 500], 46 | [10, 70], 47 | [20, 140], 48 | [50, 350], 49 | [100, 700], 50 | [10, 100], 51 | [20, 200], 52 | [50, 500], 53 | [100, 1000]]; 54 | 55 | 56 | fnN = length(functions); 57 | sizesN = size(sizes, 1); 58 | 59 | timings = cell(sizesN, fnN); 60 | 61 | 62 | for i_MN = 1:sizesN 63 | M = sizes(i_MN, 1); 64 | N = sizes(i_MN, 2); 65 | fprintf('%i, %ix%i\n', i_MN, M, N ); 66 | 67 | % how many repetitions? 68 | % we want at least 3 69 | repetitionN = max(3, ceil(repC/(N*M))); 70 | 71 | t = zeros(repetitionN, fnN); 72 | 73 | for i_rep = 1:repetitionN 74 | cost_matrix = data_fn(M, N); 75 | % if the matrix is rectangular, we want it to have more 76 | % cols than rows: 77 | assert(size(cost_matrix, 1) <= size(cost_matrix, 2)); 78 | 79 | % for all functions: 80 | for i_fn = 1:fnN 81 | fn = functions{i_fn}; 82 | 83 | tic 84 | fn(cost_matrix); 85 | t(i_rep, i_fn) = toc; 86 | end 87 | 88 | 89 | end 90 | 91 | % keep the timings for this setting: 92 | for i_fn = 1:fnN 93 | timings{i_MN, i_fn} = t(:, i_fn); 94 | end 95 | end 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | function d2=dist2(x,y) 104 | % function d2=dist2(x,y) 105 | % 106 | % INPUTS: 107 | % x: D-by-M matrix which stacks M vectors of dimension D 108 | % y: D-by-N matrix which stacks N vectors of dimension D 109 | % 110 | % OUTPUT: 111 | % d2: M-by-N matrix 112 | % element (k,l) is a squared distance between 113 | % k-th vector in x and l-th vector in y 114 | 115 | xN=size(x,2); 116 | yN=size(y,2); 117 | 118 | d2=repmat(sum(x.^2),[yN 1])' + repmat(sum(y.^2),[xN 1]) - 2*x'*y; 119 | 120 | 121 | -------------------------------------------------------------------------------- /linear_sum_assignment.m: -------------------------------------------------------------------------------- 1 | function [i, j] = linear_sum_assignment(cost_matrix) 2 | % function [i, j] = linear_sum_assignment(cost_matrix) 3 | % 4 | % Hungarian algorithm (Kuhn-Munkres) for solving the linear sum assignment 5 | % problem. 6 | % 7 | % Input: 8 | % COST_MATRIX: n-by-m matrix with costs. Rectangular cost matrices 9 | % are supported. 10 | % 11 | % Output: 12 | % I, J: matching. 13 | % Row I(k) is matched to column J(k), 14 | % k = 1, 2, 3, ... min(size(COST_MATRIX)). 15 | % 16 | 17 | % Based on python implementation by Brian Clapper and Gael Varoquaux. 18 | % Adapted to matlab by Ondrej Drbohlav. 19 | % 20 | % Copyright (c) 2008 Brian M. Clapper , Gael Varoquaux 21 | % Copyright (c) 2016 Ondrej Drbohlav 22 | % License: 3-clause BSD 23 | 24 | True = 1; 25 | False = 0; 26 | 27 | if length(size(cost_matrix)) ~= 2 28 | error(sprintf('Expected a matrix (2-d array), got a %i-d array'), ... 29 | length(size(cost_matrix))); 30 | end 31 | 32 | % The algorithm expects more columns than rows in the cost matrix. 33 | if size(cost_matrix, 2) < size(cost_matrix, 1) 34 | cost_matrix = cost_matrix'; 35 | transposed = True; 36 | else 37 | transposed = False; 38 | end 39 | 40 | state = Hungary_(cost_matrix); 41 | 42 | step = @step1; 43 | 44 | while ~isequal(step, @none) 45 | [step, state] = step(state); 46 | end 47 | 48 | if transposed 49 | marked = state.marked'; 50 | else 51 | marked = state.marked; 52 | end 53 | 54 | [i, j] = find(marked == 1); 55 | 56 | 57 | function s = Hungary_(cost_matrix) 58 | [n, m] = size(cost_matrix); 59 | 60 | s = struct('C', cost_matrix, ... 61 | 'row_uncovered', true(n, 1), ... 62 | 'col_uncovered', true(1, m), ... 63 | 'Z0_r', 1, ... 64 | 'Z0_c', 1, ... 65 | 'path', ones(n+m, 2, 'int64'), ... 66 | 'marked', zeros(n, m, 'int8')); 67 | 68 | function none() 69 | return 70 | 71 | function state = clear_covers(state) 72 | True = 1; 73 | False = 0; 74 | % """Clear all covered matrix cells""" 75 | state.row_uncovered(:) = True; 76 | state.col_uncovered(:) = True; 77 | 78 | %# Individual steps of the algorithm follow, as a state machine: they return 79 | %# the next step to be taken (function to be called), if any. 80 | 81 | function [fnhandle, state] = step1(state) 82 | True = 1; 83 | False = 0; 84 | %"""Steps 1 and 2 in the Wikipedia page.""" 85 | % 86 | % # Step 1: For each row of the matrix, find the smallest element and 87 | % # subtract it from every element in its row. 88 | state.C = state.C - ... 89 | repmat(min(state.C,[],2), [1, size(state.C, 2)]); 90 | % # Step 2: Find a zero (Z) in the resulting matrix. If there is no 91 | % # starred zero in its row or column, star Z. Repeat for each element 92 | % # in the matrix. 93 | [i_, j_] = find(state.C == 0); 94 | 95 | for k = 1:length(i_) 96 | i = i_(k); 97 | j = j_(k); 98 | if state.col_uncovered(j) & state.row_uncovered(i) 99 | state.marked(i, j) = 1; 100 | state.col_uncovered(j) = False; 101 | state.row_uncovered(i) = False; 102 | end 103 | end 104 | 105 | state = clear_covers(state); 106 | fnhandle = @step3; 107 | 108 | function [fnhandle, state] = step3(state) 109 | True = 1; 110 | False = 0; 111 | % """ 112 | % Cover each column containing a starred zero. If n columns are covered, 113 | % the starred zeros describe a complete set of unique assignments. 114 | % In this case, Go to DONE, otherwise, Go to Step 4. 115 | % """ 116 | marked = (state.marked == 1); 117 | state.col_uncovered( any(marked) ) = False; 118 | 119 | if sum(marked(:)) < size(state.C, 1) 120 | fnhandle = @step4; 121 | else 122 | fnhandle = @none; 123 | end 124 | 125 | function [fnhandle, state] = step4(state) 126 | True = 1; 127 | False = 0; 128 | % """ 129 | % Find a noncovered zero and prime it. If there is no starred zero 130 | % in the row containing this primed zero, Go to Step 5. Otherwise, 131 | % cover this row and uncover the column containing the starred 132 | % zero. Continue in this manner until there are no uncovered zeros 133 | % left. Save the smallest uncovered value and Go to Step 6. 134 | % """ 135 | % # We convert to int as numpy operations are faster on int 136 | 137 | [n, m] = size(state.C); 138 | 139 | % covered_C: bool 140 | covered_C = logical(state.C == 0); 141 | covered_C(~state.row_uncovered, :) = 0; 142 | covered_C(:, ~state.col_uncovered, :) = 0; 143 | 144 | while True 145 | %# Find an uncovered zero 146 | idx_zero = find(covered_C, 1); 147 | 148 | if length(idx_zero) == 0 149 | fnhandle = @step6; 150 | return 151 | else 152 | row = mod(idx_zero-1, n)+1; col = floor((idx_zero-1)/n)+1; 153 | state.marked(row, col) = 2; 154 | %# Find the first starred element in the row 155 | star_col = find(state.marked(row, :) == 1, 1); 156 | if isempty(star_col) 157 | %# Could not find one 158 | state.Z0_r = row; 159 | state.Z0_c = col; 160 | fnhandle = @step5; 161 | return 162 | else 163 | col = star_col; 164 | state.row_uncovered(row) = False; 165 | state.col_uncovered(col) = True; 166 | covered_C(~state.row_uncovered, col) = 0 ; 167 | covered_C(row, :) = 0; 168 | end 169 | end 170 | end 171 | 172 | 173 | function [fnhandle, state] = step5(state) 174 | True = 1; 175 | False = 0; 176 | % """ 177 | % Construct a series of alternating primed and starred zeros as follows. 178 | % Let Z0 represent the uncovered primed zero found in Step 4. 179 | % Let Z1 denote the starred zero in the column of Z0 (if any). 180 | % Let Z2 denote the primed zero in the row of Z1 (there will always be one%). 181 | % Continue until the series terminates at a primed zero that has no starred 182 | % zero in its column. Unstar each starred zero of the series, star each 183 | % primed zero of the series, erase all primes and uncover every line in th%e 184 | % matrix. Return to Step 3 185 | % """ 186 | el0 = 1; 187 | el1 = 2; 188 | [n, m] = size(state.C); 189 | 190 | count = 1; 191 | state.path(count, el0) = state.Z0_r; 192 | state.path(count, el1) = state.Z0_c; 193 | 194 | while True 195 | %# Find the first starred element in the col defined by 196 | %# the path. 197 | row = find(state.marked(:, state.path(count, el1) ) == 1, 1); 198 | if isempty(row) 199 | %# Could not find one 200 | break; 201 | else 202 | count = count + 1; 203 | state.path(count, el0) = row; 204 | state.path(count, el1) = state.path(count - 1, el1); 205 | end 206 | 207 | %# Find the first prime element in the row defined by the 208 | %# first path step 209 | col = find(state.marked( state.path(count, el0), : ) == 2, 1); 210 | % Note: there will always be such col, thus we do not test 211 | % for isempty(col) 212 | count = count + 1; 213 | state.path(count, el0) = state.path(count - 1, el0); 214 | state.path(count, el1) = col; 215 | end 216 | 217 | %# Convert paths: 218 | for i = 1:(count) 219 | if state.marked( state.path(i, el0), state.path(i, el1) ) == 1 220 | state.marked( state.path(i, el0), state.path(i, el1)) = 0; 221 | else 222 | state.marked( state.path(i, el0), state.path(i, el1) ) = 1; 223 | end 224 | end 225 | 226 | state = clear_covers(state); 227 | %# Erase all prime markings 228 | state.marked(state.marked == 2) = 0; 229 | fnhandle = @step3; return 230 | 231 | 232 | function [fnhandle, state] = step6(state) 233 | % """ 234 | % Add the value found in Step 4 to every element of each covered row, 235 | % and subtract it from every element of each uncovered column. 236 | % Return to Step 4 without altering any stars, primes, or covered lines. 237 | % """ 238 | % # the smallest uncovered value in the matrix 239 | if any(state.row_uncovered) & any(state.col_uncovered) 240 | minval = min(min(state.C(state.row_uncovered, ... 241 | state.col_uncovered))); 242 | state.C(~state.row_uncovered, :) = state.C(~state.row_uncovered, :) ... 243 | + minval; 244 | state.C(:, state.col_uncovered) = state.C(:, state.col_uncovered) ... 245 | - minval; 246 | end 247 | 248 | fnhandle = @step4; --------------------------------------------------------------------------------