Hungarian Algorithm

Problem Statement

The problem of the assignment is fairly straight-forward. To describe it, let us consider several examples

  • There are workers and tasks. You know exactly how much you need to pay each worker to perform one or another task. You also know that every worker can only perform one task. Your goal is to assign each worker some a task, while minimizing your expenses.
  • Given matrix of size , you need to select one element per row and column, such that the sum of all selected cells is minimized.
  • Given a matrix of size , find permutation of length , such taht is minimized
  • Given a complete bipartite graph with vertices with every edge having some weight. Find perfect matching for the graph with minimum weight.

Note: All the problems above assume a “square matrix”, that is dimensions. In practice this is rarely the case, and the dimensions are . In this case the goal is to match vertices.

Note: The minimization problem above could be converted to maximization problem by multiplying all weights by .

Most info here comes from here (Russian)

History

Hungarian algorithm was developed and published by Harold Kuhn in 1955. Kuhn named the algorithm “Hungarian” because he based his work on the works of two hungarian ,athematicians, Dénis König and Jenö Egerváry.

In 1957 James Munkres has shown that Hungarian algorithm is strictly polynomial. Because of the contributions of Munkres, the algorithm is sometimes referred to as “Kuhn-Munkres algorithm”, “Kuhn algorithm”, or “Munkres algorithm”. Original algorithm had asymptotic complexity , and it were Jack Edmonds, Richard Karp, and Tomizawa have shown a version of the algorithm with complexity.

Algorithm description

To avoid any future confusion let us state beforehand taht we are considering the assignment problem in the matrix context (i.e. given matrix , pick cells in different rows and columns). Indexing of the arrays in the current analysis will be 1-based, i.e. . Also, note that all numbers in are non-negative. If there are negative numbers in the matrix, you can always add some value to all elements to make them positive.

algorithm

Rephrasing the Wiki page, there are two arbitrary arrays and defined as potential, such that

Note that and correspond to rows and columns of matrix respectively.

In that case, we can define the value of the potential as

From one side one can see that the cost of the final optimal solution is not less than the potential (proof left as an exercise for the reader):

From another side, it appears, there is always a solution and a potential where the inequality turns into the equality (Hungarian algorithm is the proof of it). Meanwhile, note that if some solution has some associated cost equal to its potential, this solution will be considered optimal.

Let’s call the edge tight iff:

In terms of a bipartite graphs, we can view this assumptions as follows:

Given a bipartite graph consisting of only tight edges, the Hungarian algorithm maintains the matched pairs list which maximizes the number of pairs for a given potential . The moment has elements, the edges of this pairs list will be the optimal solution (it is optimal because the potential will be equal to the solution cost).

Now to the algorithm istself

Given two parts of the bipartite graph: first and second ,

  1. Initially the potential is zero , and the pair list is empty
  2. While maintaining the potential, try to find maximum number of matches. To achieve that we can use any matching algorithm. Hint: BFS and DFS can be quite efficient at this step.
    • If the solution is found, return the solution
    • If the solution is not found, but the optimal pairset was updated, repeat this step
    • If the solution is not found, and the optimal pairset was not updated, go to the next step.
  3. Modify the way you acquire the pairs, and the potentials, and go to step 2. One way of doing it is, if you were looking for pairs from to , now look from to – from matrix point of view, if you were looking into minimizing the rows cost, continue with minimizing the column costs (not really! see the note below).

Note on step 3: Formally, this step is a way of changing the potentials and , such that a viable solution is still possible. Without providing the proofs, say we have and , such that contain the visited vertices. In that case we can compute strictly positive potential gain

Now we can update the potentials, without the total potential being different, as follows

Note that even though we changed the potential, the partially computed is still valid. At this point we can note that potential changes cannot continue indefinitely, because at every iteration the number of visited vertices that could be connected only increases (can only increase However, we cannot claim that the number of tight edges increases). That means that the maximum number of changes in the potential that can happen does not exceed .

algorithm

Now, let us describe a algorithm (or a for non-square matrices).

Key idea: Don’t consider the whole matrix at once, but append a new row at every iteration. In that case the algorithm above could be rewritten as:

  1. Add the next row in to the “used” list
  2. While the is not increasing, that is the number of edges that originate in the current row doesn’t increase (or the potential for this row is non-zero), keep on changing the potentials using the Note on step 3 described above.
  3. The moment we notice that the number of pairs has increased, that is the potential for the vertex is reduced to zero, we add this vertex to the found pairset by iterating through all the found pairsets that were found before, and repeat the process for the next row (step 1).

Steps 2 and 3 have asymptotic complexity of , and they have to be repeated times. The proof for this is left as an exercise for the reader.

To achieve the given asymptotics, we need to note the following key ideas:

  • To find if the pairset size started increasing, we don’t need to revisit all the nodes to check for potential changes (Kuhn traversal). Instead, after each update of the potentials, we look at the vertex of the edge, and if it was previously visited, we mark the side of the vertex as visited as well, and continue iterating from that point on.
  • Given the previous key idea, we can amend the algorithm: at every iteration of the algorithm, if we are updating the potentials, we check which of the columns ( vertex) was just found. We check if this column is already used, and if not we found another pair to add to . At this point, we mark the column as used.
  • To compute the potentials quickly, we can keep track of thew minimum vertex found for every column , which will be updated every time the potentials change.

Now we can find in linear time.

Hungarian algorithm implementation in time

Given a matrix , rows represent vertices of part of the bipartite graph, and columns represent the other part . The matching needs to be done from rows to columns.

The codes in this section are written by Andrej Lopatin.

The code below solves the matching problem for a matrix , where . It uses 1-indexing to simplify and shorted the codes: the “fake-0” index is used as a flag, and avoids additional checks if the computation was done :).

Arrays and store the potentials, which are initilized to 0 (same as 0-row matrix). Also, the current implementation works even if there are negative numbers in the edge weights.

Array holds information about matched pairs: for every column , it holds the value of the corresponding row (or 0 if still no matching found). keeps track of the current row, which is done to simplify the code and avoid checks.

Array holds the temporary minimums in the potentials for each column such that . This is used for a quick re-computation and update of the potentials.

Array holds information about where the minimums are achieved, and is used to reconstruct the graph matching. One might think that array should hold information about every row and that we require additional array: for every row we need to keep track of the corresponding column. However, note taht Kuhn algorithm reaches the rows from columns after visiting edges of that pair. That’s why to reconstruct the matching of the graph, we can always get the rows from the matched pair array (i.e. ). Thus, has information about the previous column, or 0 if there is no match.

The implementation of the algorithm includes a for-loop over the rows of the matrix. Inside the loop we keep track of the current row by placing it in the , and start a loop which finds the first unused column . Every iteration of the loop marks column as used (previously found column), and gets the appropriate pair . At this point, because we have a new used row , we recompute the , and find while we are at it. We also find in which column the minimum was found. If is zero, we don’t have to update the potential – we already have a matching pair. After that potentials are updated keeping in mind the changes we introduced in . After the loop finishes, we will have found a chain of pairs, and to reconstruct the pairset, we can “unwind” using the ancestry array .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Given the cost matrix "vector<vector<int>> A {...};"
// Find the maximum matching "vector<pair<int,int>>result;" with all pairs
// As well as total cost "int C;" with the minimum assignment cost.
vector<int> u (n+1), v (m+1), p (m+1), way (m+1);
for (int i=1; i<=n; ++i) {
	p[0] = i;
	int j0 = 0;
	vector<int> minv (m+1, INF);
	vector<char> used (m+1, false);
	do {
		used[j0] = true;
		int i0 = p[j0],  delta = INF,  j1;
		for (int j=1; j<=m; ++j)
			if (!used[j]) {
				int cur = A[i0][j]-u[i0]-v[j];
				if (cur < minv[j])
					minv[j] = cur,  way[j] = j0;
				if (minv[j] < delta)
					delta = minv[j],  j1 = j;
			}
		for (int j=0; j<=m; ++j)
			if (used[j])
				u[p[j]] += delta,  v[j] -= delta;
			else
				minv[j] -= delta;
		j0 = j1;
	} while (p[j0] != 0);
	do {
		int j1 = way[j0];
		p[j0] = p[j1];
		j0 = j1;
	} while (j0);
}

vector<pair<int, int>> result;
for (int i = 1; i <= m; ++i){
  result.push_back(make_pair(p[i], i));
}

int C = -v[0];

References

E-Maxx

Wiki article

Zafar Takhirov

Zafar Takhirov
I am a recent PhD graduate from Boston University. While my work focuses on digital design,error mitigation, and machine learning, my non-work interests range widely from information theory (go Shannon!), quantum computing, grandfather paradox, Star Trek, Little Mermaid, 'why is the grass green?', 1Q84, etc., etc., etc. If you want to talk about, well, anything - just ping me.

Passing cv::Mat as argument

Often times when we pass `cv::Mat`, we forget one important thing: `OpenCV` matrix does not respect the `const` modifier.In this post I w...… Continue reading

Adaptive Classification and More

Published on February 13, 2017

not_notMNIST Dataset Generation

Published on January 19, 2017