What is the Hungarian Algorithm?
The Hungarian Algorithm (also called the Kuhn-Munkres Algorithm) is a combinatorial optimization algorithm that solves the Assignment Problem in polynomial time: O(N³). It finds the minimum weight perfect matching in a weighted bipartite graph, mapping workers to tasks to minimize total cost.
Linear Programming Formulation
- The assignment problem can be formally modeled as a Linear Program (LP).
1. Primal LP Formulation
- Let be the cost of assigning worker to task . Let be a decision variable where if worker is assigned to task , and otherwise.
- Subject to the constraints:
- (Due to the Birkhoff-von Neumann theorem, the extreme points of this LP are integer-valued, so we do not need to explicitly enforce ).
2. Dual LP Formulation
- The dual problem introduces potentials for workers (rows) and for tasks (columns):
- Subject to the constraint:
- Under the Complementary Slackness theorem, if , then we must have .
- The Hungarian algorithm maintains these potentials and and iteratively updates them until a perfect matching can be formed using only tight edges where (the zero-cost slots).
Graph Theory Foundations & König’s Theorem
Bipartite Graphs & Matching
- We represent workers and tasks as a bipartite graph . A matching is a set of edges without common vertices. A perfect matching matches every vertex in the graph.
Kőnig’s Theorem (1931)
-
Kőnig's Theorem maximum matching is equal to the size of a minimum vertex cover (the minimum number of vertices needed to touch all edges).
In any bipartite graph, the size of a
- In the Hungarian algorithm, Step 3 requires us to find the minimum number of horizontal and vertical lines to cover all zeros.
- Since covering a zero with a row-line or col-line corresponds to choosing a row-vertex or col-vertex to cover a zero-edge in the bipartite graph of zero costs:
- Min Lines to Cover Zeros = Min Vertex Cover = Max Matching of Zeros
- If the maximum matching of zero-cost cells is , we can make a perfect assignment using only zeros, which means our dual solutions are tight and optimal.
Programmatic Line-Drawing Algorithm (Minimum Vertex Cover)
- To find the minimum covering lines programmatically:
-
- Find a maximum matching in the bipartite graph of zero-cost cells.
-
- Mark all unmatched rows.
-
- For each newly marked row, mark all columns that have a zero in that row.
-
- For each newly marked column, mark the row that is currently matched with it.
-
- Repeat steps 3 and 4 until no more rows or columns can be marked.
-
- Draw lines through all unmarked rows and all marked columns. This set of lines covers all zeros with the minimum count .
Complete 4x4 Matrix Reduction Trace
- Let’s minimize the cost for the following 4x4 cost matrix:
Step 1: Row Reduction
- Row minima: Row 1 = 2, Row 2 = 3, Row 3 = 1, Row 4 = 4.
- Subtract minima:
Step 2: Column Reduction
- Column minima of : Col 1 = 3, Col 2 = 0, Col 3 = 0, Col 4 = 0.
- Subtract minima:
Step 3: Draw Lines to Cover Zeros
- Zeros are at , , , , , .
- Let’s cover them:
- Line 1: Col 1 (covers and )
- Line 2: Col 3 (covers and )
- Line 3: Row 1 (covers )
- Line 4: Row 4 (covers ) Wait! Can we cover all zeros with 3 lines? Let’s check:
- Col 1 (covers , )
- Col 3 (covers , )
- Row 1 (covers )
- Row 4 (covers )
Is there a way with 3 lines?
If we select Col 1, Col 3, and Row 1:
- Remaining uncovered zeros: . We need a line for it (either Row 4 or Col 4). So we need at least 4 lines to cover all zeros. Since the number of lines , we already have our optimal matching!
Step 4: Final Optimal Assignment
- Find rows/columns with a single zero first:
- Row 1: Zero at Col 2 Worker 1 Task 2 (Original Cost = 2).
- Row 3: Zero at Col 3 Worker 3 Task 3 (Original Cost = 1).
- Row 4: Zeros at Col 1 and Col 4. Since Col 1 is assigned to Row 2, assign Worker 4 Task 4 (Original Cost = 4).
- Row 2: Zero at Col 1 Worker 2 Task 1 (Original Cost = 6).
- Total Minimum Cost = .
Variations and Edge Cases
1. Maximization Problems
- To maximize total profit, multiply all elements by or subtract all elements from the maximum element in the matrix. Then solve as a minimization problem.
2. Unbalanced Assignment (Rectangular Matrices)
- If the number of workers is not equal to the number of tasks :
- Pad the matrix with dummy rows or columns filled with s to make it a square matrix of size .
3. Forbidden Assignments
- If worker cannot perform task , set (or a very large constant ).
Time & Space Complexity
Complexity Table
| Implementation | Time Complexity | Space Complexity | Details |
|---|---|---|---|
| Brute Force | Checked via permutations | ||
| Hungarian Algorithm | Efficient potentials augmentation |
Implementation
-
Below are the implementations in Python (using SciPy and a custom solver), C++, JavaScript, and Java. Python · Cpp · Java Script · Java
Languages:
# 1. Standard Library Implementation (Requires SciPy)
from scipy.optimize import linear_sum_assignment
def solve_assignment_scipy(cost_matrix):
row_ind, col_ind = linear_sum_assignment(cost_matrix)
total_cost = sum(cost_matrix[r][c] for r, c in zip(row_ind, col_ind))
return list(zip(row_ind, col_ind)), total_cost
# Example
costs = [
[9, 2, 7, 8],
[6, 4, 3, 7],
[5, 8, 1, 8],
[7, 6, 9, 4]
]
assignments, total = solve_assignment_scipy(costs)
print("SciPy Assignment:", assignments, "Total Cost:", total)#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
// Self-contained O(N^3) Hungarian implementation
class Hungarian {
private:
int n;
std::vector<std::vector<double>> cost;
std::vector<double> u, v;
std::vector<int> p, way;
public:
Hungarian(const std::vector<std::vector<double>>& cost_matrix) {
n = cost_matrix.size();
cost = cost_matrix;
u.assign(n + 1, 0);
v.assign(n + 1, 0);
p.assign(n + 1, 0);
way.assign(n + 1, 0);
}
double solve(std::vector<int>& assignment) {
for (int i = 1; i <= n; ++i) {
p[0] = i;
int j0 = 0;
std::vector<double> minv(n + 1, 1e18);
std::vector<bool> used(n + 1, false);
do {
used[j0] = true;
int i0 = p[j0], j1 = 0;
double delta = 1e18;
for (int j = 1; j <= n; ++j) {
if (!used[j]) {
double cur = cost[i0 - 1][j - 1] - 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 <= n; ++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 != 0);
}
assignment.assign(n, 0);
for (int j = 1; j <= n; ++j) {
assignment[p[j] - 1] = j - 1;
}
return -v[0];
}
};
int main() {
std::vector<std::vector<double>> cost_matrix = {
{9, 2, 7, 8},
{6, 4, 3, 7},
{5, 8, 1, 8},
{7, 6, 9, 4}
};
Hungarian solver(cost_matrix);
std::vector<int> assignment;
double min_cost = solver.solve(assignment);
std::cout << "Optimal assignment:\n";
for (int i = 0; i < cost_matrix.size(); ++i) {
std::cout << "Worker " << i << " -> Task " << assignment[i] << "\n";
}
std::cout << "Total Cost: " << min_cost << "\n";
return 0;
}// Self-contained Hungarian Algorithm
function hungarian(costMatrix) {
const n = costMatrix.length;
const u = new Array(n + 1).fill(0);
const v = new Array(n + 1).fill(0);
const p = new Array(n + 1).fill(0);
const way = new Array(n + 1).fill(0);
for (let i = 1; i <= n; i++) {
p[0] = i;
let j0 = 0;
const minv = new Array(n + 1).fill(Infinity);
const used = new Array(n + 1).fill(false);
do {
used[j0] = true;
const i0 = p[j0];
let j1 = 0;
let delta = Infinity;
for (let j = 1; j <= n; j++) {
if (!used[j]) {
const cur = costMatrix[i0 - 1][j - 1] - u[i0] - v[j];
if (cur < minv[j]) {
minv[j] = cur;
way[j] = j0;
}
if (minv[j] < delta) {
delta = minv[j];
j1 = j;
}
}
}
for (let j = 0; j <= n; j++) {
if (used[j]) {
u[p[j]] += delta;
v[j] -= delta;
} else {
minv[j] -= delta;
}
}
j0 = j1;
} while (p[j0] !== 0);
do {
const j1 = way[j0];
p[j0] = p[j1];
j0 = j1;
} while (j0 !== 0);
}
const assignment = new Array(n);
for (let j = 1; j <= n; j++) {
assignment[p[j] - 1] = j - 1;
}
return { assignment, cost: -v[0] };
}
const costs = [
[9, 2, 7, 8],
[6, 4, 3, 7],
[5, 8, 1, 8],
[7, 6, 9, 4]
];
const res = hungarian(costs);
console.log("Assignments:", res.assignment);
console.log("Total Cost:", res.cost);import java.util.*;
public class HungarianAlgorithm {
public static double solve(double[][] costMatrix, int[] assignment) {
int n = costMatrix.length;
double[] u = new double[n + 1];
double[] v = new double[n + 1];
int[] p = new int[n + 1];
int[] way = new int[n + 1];
for (int i = 1; i <= n; i++) {
p[0] = i;
int j0 = 0;
double[] minv = new double[n + 1];
Arrays.fill(minv, Double.MAX_VALUE);
boolean[] used = new boolean[n + 1];
do {
used[j0] = true;
int i0 = p[j0];
int j1 = 0;
double delta = Double.MAX_VALUE;
for (int j = 1; j <= n; j++) {
if (!used[j]) {
double cur = costMatrix[i0 - 1][j - 1] - 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 <= n; 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 != 0);
}
for (int j = 1; j <= n; j++) {
assignment[p[j] - 1] = j - 1;
}
return -v[0];
}
public static void main(String[] args) {
double[][] costs = {
{9, 2, 7, 8},
{6, 4, 3, 7},
{5, 8, 1, 8},
{7, 6, 9, 4}
};
int[] assign = new int[costs.length];
double cost = solve(costs, assign);
System.out.println("Assignments: " + Arrays.toString(assign));
System.out.println("Total Cost: " + cost);
}
}
When to Use Hungarian Matching
✅ Use When:
- You want to assign workers to tasks or map pairs in bipartite graphs while minimizing total cost.
- Matrix dimensions . The complexity makes it exceptionally fast for medium-scale matching tables.
❌ Avoid When:
- The graph is highly sparse — use Min-Cost Max-Flow (MCMF) algorithms or successive shortest paths on graph networks which perform better on sparse edges.
- The coordinates represent geometric positions — specialized geometric matching algorithms exist that can solve assignments in better time.
Key Takeaways
- LP Dual Potentials — By maintaining potentials and matching only slack edges (), the algorithm constructs an optimal assignment.
- Kőnig’s Theorem — Validates that the minimum lines drawn to cover zeros is equal to the size of the maximum zero matching.
- Complexity — Polynomial bounds replace the NP-Hard permutations () brute-force.
More Learn
GitHub & Webs
Related Pages
- Ackermann Function – Theoretical growth bounds
- Euclidean Algorithm for GCD – Fundamental number theory
- DSA Algo & System Design – Full DSA index