Hungarian algorithm: Difference between revisions

Content deleted Content added
WikiCleanerBot (talk | contribs)
m v2.05b - Bot T20 CW#61 - Fix errors for CW project (Reference before punctuation)
 
(20 intermediate revisions by 14 users not shown)
Line 2:
The '''Hungarian method''' is a [[combinatorial optimization]] [[algorithm]] that solves the [[assignment problem]] in [[polynomial time]] and which anticipated later [[Duality (optimization)|primal–dual methods]]. It was developed and published in 1955 by [[Harold Kuhn]], who gave it the name "Hungarian method" because the algorithm was largely based on the earlier works of two Hungarian mathematicians, [[Dénes Kőnig]] and [[Jenő Egerváry]].<ref name="kuhn1955">Harold W. Kuhn, "The Hungarian Method for the assignment problem", ''[[Naval Research Logistics Quarterly]]'', '''2''': 83–97, 1955. Kuhn's original publication.</ref><ref name="kuhn1956">Harold W. Kuhn, "Variants of the Hungarian method for assignment problems", ''Naval Research Logistics Quarterly'', '''3''': 253–258, 1956.</ref> However, in 2006 it was discovered that [[Carl Gustav Jacobi]] had solved the assignment problem in the 19th century, and the solution had been published posthumously in 1890 in Latin.<ref>{{Cite web|url=http://www.lix.polytechnique.fr/~ollivier/JACOBI/presentationlEngl.htm|archive-url = https://web.archive.org/web/20151016182619/http://www.lix.polytechnique.fr/~ollivier/JACOBI/presentationlEngl.htm|archive-date = 16 October 2015|title = Presentation}}</ref>
 
[[James Munkres]] reviewed the algorithm in 1957 and observed that it is [[Time complexity#Strongly and weakly -polynomial time|(strongly) polynomial]].<ref name="munkres">J. Munkres, "Algorithms for the Assignment and Transportation Problems", ''[[Journal of the Society for Industrial and Applied Mathematics]]'', '''5'''(1):32–38, 1957 March.</ref> Since then the algorithm has been known also as the '''Kuhn–Munkres algorithm''' or '''Munkres assignment algorithm'''. The [[Computational complexity theory#Time and space complexity|time complexity]] of the original algorithm was <math>O(n^4)</math>, however [[Jack Edmonds|Edmonds]] and [[Richard Karp|Karp]], and independently Tomizawa, noticed that it can be modified to achieve an <math>O(n^3)</math> running time.<ref>{{Cite journal|last1=Edmonds|first1=Jack|last2=Karp|first2=Richard M.|date=1972-04-01|title=Theoretical Improvements in Algorithmic Efficiency for Network Flow Problems|journal=Journal of the ACM|volume=19|issue=2|pages=248–264|language=EN|doi=10.1145/321694.321699|s2cid=6375478|doi-access=free}}</ref><ref>{{Cite journal|last=Tomizawa|first=N.|date=1971|title=On some techniques useful for solution of transportation network problems|journal=Networks|language=en|volume=1|issue=2|pages=173–194|doi=10.1002/net.3230010206|issn=1097-0037}}</ref> One of the most popular{{Citation needed|date=November 2019}} <math>O(n^3)</math> variants is the Jonker–Volgenant algorithm.<ref name="JVAlg">{{cite journal |last1=Jonker |first1=R. |last2=Volgenant |first2=A. |title=A shortest augmenting path algorithm for dense and sparse linear assignment problems |journal=Computing |date=December 1987 |volume=38 |issue=4 |pages=325–340 |doi=10.1007/BF02278710|s2cid=7806079 }}</ref> [[L. R. Ford, Jr.|Ford]] and [[D. R. Fulkerson|Fulkerson]] extended the method to general maximum flow problems in form of the [[Ford–Fulkerson algorithm]].
 
==The problem==
Line 8:
 
=== Example ===
In this simple example, there are three workers: Alice, Bob and DoraCarol. One of them has to clean the bathroom, another sweep the floors and the third washes the windows, but they each demand different pay for the various tasks. The problem is to find the lowest-cost way to assign the jobs. The problem can be represented in a [[Matrix (mathematics)|matrix]] of the costs of the workers doing the jobs. For example:
:{| class="wikitable" style="text-align:center;"
! {{diagonal split header|Worker &nbsp; &nbsp;|Task}}
Line 25:
| '''$3'''
|-
! DoraCarol
| $9
| '''$4'''
Line 31:
|}
 
The Hungarian method, when applied to the above table, would give the minimum cost: this is $15, achieved by having Alice clean the bathroom, DoraCarol sweep the floors, and Bob wash the windows. This can be confirmed using brute force:
:{| class="wikitable" style="text-align:center;"
! {{diagonal split header|Sweep|Clean}}
! Alice
! Bob
! DoraCarol
|-
! Alice
Line 48:
| $18
|-
! DoraCarol
| '''$15'''
| $16
Line 55:
 
=== Matrix formulation ===
In the matrix formulation, we are given a nonnegativean ''n''×''n'' [[Matrix (mathematics)|matrix]], where the element in the ''i''-th row and ''j''-th column represents the cost of assigning the ''j''-th job to the ''i''-th worker. We have to find an assignment of the jobs to the workers, such that each job is assigned to one worker and each worker is assigned one job, such that the total cost of assignment is minimum.
 
This can be expressed as permuting the rows of a cost matrix ''C'' to minimize the [[Trace (linear algebra)|trace]] of a matrix,
Line 68:
 
=== Bipartite graph formulation ===
The algorithm can equivalently be described by formulating the problem using a bipartite graph. We have a [[complete bipartite graph]] <math>G=(S, T; E)</math> with {{mvar|n}} worker vertices ({{mvar|S}}) and {{mvar|n}} job vertices ({{mvar|T}}), and the edges ({{mvar|E}}) each have a nonnegative cost <math>c(i,j)</math>. We want to find a [[perfect matching]] with a minimum total cost.
 
==The algorithm in terms of bipartite graphs==
Let us call a function <math>y: (S\cup T) \to \mathbb{R}</math> a '''potential''' if <math>y(i)+y(j) \leq c(i, j)</math> for each <math>i \in S, j \in T</math>. The ''value'' of potential {{mvar|y}} is the sum of the potential over all vertices: <math>\sum_{v\in S\cup T} y(v)</math>.
 
The ''value'' of potential {{mvar|y}} is the sum of the potential over all vertices:

:<math>\sum_{v\in S\cup T} y(v)</math>.

The cost of each perfect matching is at least the value of each potential: the total cost of the matching is the sum of costs of all edges it contains; the cost of each edge is at least the sum of potentials of its endpoints; since the matching is perfect, each vertex is an endpoint of exactly one edge; hence the total cost is at least the total potential.
 
The Hungarian method finds a perfect matching and a potential such that the matching cost equals the potential value. This proves that both of them are optimal. In fact, the Hungarian method finds a perfect matching of '''tight edges''': an edge <math>ij</math> is called tight for a potential {{mvar|y}} if <math>y(i)+y(j) = c(i, j)</math>. Let us denote the [[Glossary of graph theory#Subgraphs|subgraph]] of '''tight''' edges by <math>G_y</math>. The cost of a perfect matching in <math>G_y</math> (if there is one) equals the value of {{mvar|y}}.
Line 87 ⟶ 91:
:<math>\Delta := \min \{c(i,j)-y(i)-y(j): i \in Z \cap S, j \in T \setminus Z\}.</math>
 
{{math|&Delta;}} is well defined because at least one such edge <math>ij</math> must exist whenever the matching is not yet of maximum possible size (see the following section); it is positive because there are no tight edges between <math>Z \cap S</math> and <math>T \setminus Z</math>. Increase {{mvar|y}} by {{math|&Delta;}} on the vertices of <math>Z \cap S</math> and decrease {{mvar|y}} by {{math|&Delta;}} on the vertices of <math>Z \cap T</math>. The resulting {{mvar|y}} is still a potential, and although the graph <math>G_y</math> changes, it still contains {{mvar|M}} (see the next subsections). We orient the new edges from {{mvar|S}} to {{mvar|T}}. By the definition of {{math|&Delta;}} the set {{mvar|Z}} of vertices reachable from <math>R_S</math> increases (note that the number of tight edges does not necessarily increase). If the vertex added to <math>Z \cap T</math> is unmatched (that is, it is also in <math>R_T</math>), then at the next iteration the graph will have an augmenting path.
 
We repeat these steps until {{mvar|M}} is a perfect matching, in which case it gives a minimum cost assignment. The running time of this version of the method is <math>O(n^4)</math>: {{mvar|M}} is augmented {{mvar|n}} times, and in a phase where {{mvar|M}} is unchanged, there are at most {{mvar|n}} potential changes (since {{mvar|Z}} increases every time). The time sufficient for a potential change is <math>O(n^2)</math>.
Line 144 ⟶ 148:
*/
 
#includeimport <cassert>;
 
#include <iostream>
import std;
#include <limits>
 
#include <vector>
template <typename T, typename U>
using namespace std;
using Pair = std::pair<T, U>;
template <typename T>
using Vector = std::vector<T>;
 
template <typename T>
using NumericLimits = std::numeric_limits<T>;
 
/**
* @brief Checks if b < a
*
* Sets a = min(a, b)
* @param a The first parameter to check
* @param b The second parameter to check
* @tparam The type to perform the check on
* @return true if b < a
*/
template <classtypename T>
constexpr bool ckmin(T & a, const T & b) {
return b < a ? a = b, 1true : 0false;
}
 
/**
* @brief Performs the Hungarian algorithm.
*
* Given J jobs and W workers (J <= W), computes the minimum cost to assign each
* prefix of jobs to distinct workers.
Line 168 ⟶ 188:
* to assign the first (j+1) jobs to distinct workers
*/
template <typename T>
template <class T> vectorVector<T> hungarian(const vectorVector<vectorVector<T>>& &C) {
const int J = (int)size(C), W = (int)size(C[0]);
const int J = static_cast<int>(C.size());
const int W = static_cast<int>(C[0].size());
assert(J <= W);
// job[w] = job assigned to w-th worker, or -1 if no job assigned
// note: a W-th worker was added for convenience
vectorVector<int> job(W + 1, -1);
vectorVector<T> ys(J),;
Vector<T> yt(W + 1); // potentials
// -yt[W] will equal the sum of all deltas
vectorVector<T> answers;
const T inf = numeric_limitsNumericLimits<T>::max();
for (int j_curjCur = 0; j_curjCur < J; ++j_curjCur) { // assign j_curjCur-th job
int w_curwCur = W;
job[w_curwCur] = j_curjCur;
// min reduced cost over edges from Z to worker w
vectorVector<T> min_tominTo(W + 1, inf);
vectorVector<int> prvprev(W + 1, -1); // previous worker on alternating path
vectorVector<bool> in_ZinZ(W + 1); // whether worker is in Z
while (job[w_curwCur] != -1) { // runs at most j_curjCur + 1 times
in_ZinZ[w_curwCur] = true;
const int j = job[w_curwCur];
T delta = inf;
int w_nextwNext;
for (int w = 0; w < W; ++w) {
if (!in_ZinZ[w]) {
if (ckmin(min_tominTo[w], C[j][w] - ys[j] - yt[w]))
prvprev[w] = w_curwCur;
if (ckmin(delta, min_tominTo[w])) w_next = w;
wNext = w;
}
}
// delta will always be non-negativenonnegative,
// except possibly during the first time this loop runs
// if any entries of C[j_curjCur] are negative
for (int w = 0; w <= W; ++w) {
if (in_ZinZ[w]) ys[job[w]] += delta, yt[w] -= delta;{
else min_to ys[job[w]] -+= delta;
yt[w] -= delta;
} else {
minTo[w] -= delta;
}
}
w_curwCur = w_nextwNext;
}
// update assignments along alternating path
for (int w; w_curwCur != W; w_curwCur = w) job[w_cur] = job[w = prv[w_cur]];
job[wCur] = job[w = prev[wCur]];
answers.push_back(-yt[W]);
}
Line 214 ⟶ 243:
 
/**
* @brief Performs a sanity check for the Hungarian algorithm.
*
* Sanity check: https://en.wikipedia.org/wiki/Hungarian_algorithm#Example
* First job (5):
Line 222 ⟶ 253:
* First + second + third jobs (15):
* clean bathroom: Alice -> 8
* sweep floors: DoraCarol -> 4
* wash windows: Bob -> 3
*/
void sanity_check_hungariansanityCheckHungarian() {
vectorVector<vectorVector<int>> costs{{8, 5, 9}, {4, 2, 4}, {7, 3, 8}};
assert((hungarian(costs) == vectorVector<int>{5, 9, 15}));
cerr <<std::println(stderr, "Sanity check passed.\n");
}
 
/**
// * @brief solves https://open.kattis.com/problems/cordonbleu
void cordon_bleu() {
*/
int N, M;
void cordon_bleucordonBleu() {
cin >> N >> M;
vector<pair<int, int>> B(N), C(M);
int N, M;
vector<pair<int, int>> bottles(N), couriers(M);
for (auto &b std:: bottles) cin >> b.firstN >> b.secondM;
Vector<Pair<int, int>> B(N);
for (auto &c : couriers) cin >> c.first >> c.second;
pairVector<Pair<int, int>> restC(M);
vectorVector<pairPair<int, int>> bottles(N), couriers(M);
cin >> rest.first >> rest.second;
vectorVector<vectorPair<int>> costs(N, vector<int>> couriers(N + M - 1));
autofor dist(const = [&](pairPair<int, int>& x, pair<int, int>b: ybottles) {
returnstd::cin abs(x.first>> - yb.first) + abs(x.second ->> yb.second);
for (const Pair<int, int>& c: couriers)
for (auto &c : couriers) std::cin >> c.first >> c.second;
Pair<int, int> rest;
std::cin >> rest.first >> rest.second;
Vector<Vector<int>> costs(N, Vector<int>(N + M - 1));
auto dist = [&](const Pair<int, int>& x, const Pair<int, int>& y) -> int {
return std::abs(x.first - y.first) + std::abs(x.second - y.second);
};
for (int b = 0; b < N; ++b) {
for (int c = 0; c < M; ++c) { // courier -> bottle -> restaurant
costs[b][c] = dist(couriers[c], bottles[b]) + dist(bottles[b], rest);
for (int _ = 0; _ < N - 1; ++_) { // restaurant -> bottle -> restaurant
dist(couriers[c], bottles[b]) + dist(bottles[b], rest);
}
for (int _ = 0; _ < N - 1; ++_) { // restaurant -> bottle -> restaurant
costs[b][_ + M] = 2 * dist(bottles[b], rest);
}
}
cout << std::println(hungarian(costs).back() << "\n");
}
 
/**
* @brief Entry point into the program.
*
* @return The return code of the program.
*/
int main() {
sanity_check_hungariansanityCheckHungarian();
cordon_bleucordonBleu();
}
</syntaxhighlight>
Line 286 ⟶ 326:
 
<syntaxhighlight lang="cpp" line="1">
template <typename T>
template <class T> vectorVector<T> hungarian(const vectorVector<vectorVector<T>>& &C) {
const int J = (int)size(C), W = (int)size(C[0]);
const int J = (static_cast<int)size>(C), W = (int).size(C[0]));
const int W = static_cast<int>(C[0].size());
assert(J <= W);
// job[w] = job assigned to w-th worker, or -1 if no job assigned
// note: a W-th worker was added for convenience
vectorVector<int> job(W + 1, -1);
vectorVector<T> h(W); // Johnson potentials
vectorVector<T> answers;
T ans_curansCur = 0;
const T inf = numeric_limitsNumericLimits<T>::max();
// assign j_curjCur-th job using Dijkstra with potentials
for (int j_curjCur = 0; j_curjCur < J; ++j_curjCur) {
int w_curwCur = W; // unvisited worker with minimum distance
job[w_curwCur] = j_curjCur;
vectorVector<T> dist(W + 1, inf); // Johnson-reduced distances
dist[W] = 0;
vectorVector<bool> vis(W + 1); // whether visited yet
vectorVector<int> prvprev(W + 1, -1); // previous worker on shortest path
while (job[w_curwCur] != -1) { // Dijkstra step: pop min worker from heap
T min_distminDist = inf;
vis[w_curwCur] = true;
int w_nextwNext = -1; // next unvisited worker with minimum distance
// consider extending shortest path by w_curwCur -> job[w_curwCur] -> w
for (int w = 0; w < W; ++w) {
if (!vis[w]) {
// sum of reduced edge weights w_curwCur -> job[w_curwCur] -> w
T edge = C[job[w_curwCur]][w] - h[w];
if (w_curwCur != W) {
edge -= C[job[w_curwCur]][w_curwCur] - h[w_curwCur];
assert(edge >= 0); // consequence of Johnson potentials
}
if (ckmin(dist[w], dist[w_curwCur] + edge)) prv[w] = w_cur;
if (ckmin(min_dist, dist prev[w])) w_next = wwCur;
if (ckmin(minDist, dist[w]))
wNext = w;
}
}
w_curwCur = w_nextwNext;
}
for (int w = 0; w < W; ++w) { // update potentials
ckmin(dist[w], dist[w_curwCur]);
h[w] += dist[w];
}
ans_cur += h[w_curwCur];
for (int w; w_curwCur != W; w_curwCur = w) job[w_cur] = job[w = prv[w_cur]];
answers.push_back(ans_cur) job[wCur] = job[w = prev[wCur]];
answers.push_back(ansCur);
}
return answers;
Line 357 ⟶ 402:
===Step 1===
 
'''For each row, its minimum element is subtracted from every element in that row.''' This causes all elements to have non-negativenonnegative values. Therefore, an assignment with a total penalty of 0 is by definition a minimum assignment.
 
This also leads to at least one zero in each row. As such, a naive greedy algorithm can attempt to assign all workers a task with a penalty of zero. This is illustrated below.
Line 525 ⟶ 570:
If the number of starred zeros is {{mvar|n}} (or in the general case <math>min(n,m)</math>, where {{mvar|n}} is the number of people and {{mvar|m}} is the number of jobs), the algorithm terminates. See the [[#Result|Result]] subsection below on how to interpret the results.
 
'''Otherwise, find the lowest uncovered value. Subtract this from every unmarkeduncovered element and add it to every element covered by two lines. Go back to step 4.'''
 
This is equivalent to subtracting a number from all rows which are not covered and adding the same number to all columns which are covered. These operations do not change optimal assignments.
Line 564 ⟶ 609:
* [https://github.com/evansenter/gene/blob/f515fd73cb9d6a22b4d4b146d70b6c2ec6a5125b/objects/extensions/hungarian.rb Ruby implementation with unit tests]
* [https://github.com/antifriz/hungarian-algorithm-n3 C# implementation claiming <math>O(n^3)</math> time complexity]
* [http://www.fantascienza.net/leonardo/so/hungarian.d D implementation with unit tests (port of a Java version claiming <math>O(n^3)</math>)] {{Webarchive|url=https://web.archive.org/web/20191230174110/http://www.fantascienza.net/leonardo/so/hungarian.d |date=30 December 2019 }}
* [http://www.ifors.ms.unimelb.edu.au/tutorial/hungarian/welcome_frame.html Online interactive implementation]<!--Please note that this implements a variant of the algorithm as described above. -->
* [http://www.netlib.org/utk/lsi/pcwLSI/text/node220.html Serial and parallel implementations.]
* [http://www.mathworks.com/matlabcentral/fileexchange/loadFile.do?objectId=6543 Matlab and C] {{Webarchive|url=https://web.archive.org/web/20080503063209/http://www.mathworks.com/matlabcentral/fileexchange/loadFile.do?objectId=6543 |date=3 May 2008 }}
* [https://metacpan.org/module/Algorithm::Munkres Perl implementation]
* [https://github.com/saebyn/munkres-cpp C++ implementation]