Weighted matching
When every pairing has a cost, the goal changes from the most pairs to the cheapest complete assignment. The Hungarian algorithm solves it in O(n^3) by maintaining dual variables that prove optimality.
Learning goals
- Set up an assignment problem as an n × n cost matrix
- Explain dual variables, reduced costs and tight edges
- Trace the Hungarian algorithm row by row
- Say when an assignment is provably optimal
Prerequisites
Give n workers one job each so that the total cost is as low as possible. Trying all n! assignments is impossible beyond a dozen workers, yet the structure of the problem lets a cheap shortcut price every cell and discover the optimum in cubic time.
Hungarian assignment algorithm
The **assignment problem** asks for a perfect matching between n rows and n columns of minimum total cost. The **Hungarian algorithm** solves it by inserting the rows one at a time. Each insertion grows an alternating tree from the new row using only edges whose **reduced cost** c[i][j] − u[i] − v[j] is zero (tight edges). When the tree cannot reach a free column, the algorithm lowers the reduced costs along the tree's boundary by the smallest slack delta, which creates at least one new tight edge, and continues.
The implementation below is the classic O(n^3) version with 1-based potentials u (rows) and v (columns). minv[j] is the smallest reduced cost from the tree to column j, and way[j] remembers which tree column gave that minimum. When a free column is reached, walking back through way[] hands every column along the path to the previous row, which is an augmentation.
Open the animation with the 3 × 3 matrix 4 1 3 / 2 0 5 / 3 2 2. Each cell shows its reduced cost with the original cost underneath. Rows are inserted one at a time (3 dual updates in total here), and the final assignment is row 0 to column 1, row 1 to column 0, row 2 to column 2, costing 1 + 2 + 2 = 5. The last message also reports that the dual variables sum to 5; the reason that matters is explained in the dual-variables section.
1static int[] hungarian(int[][] cost) { // n x n costs; returns column assigned to each row2int n = cost.length;3int[] u = new int[n + 1], v = new int[n + 1], p = new int[n + 1], way = new int[n + 1]; // 1-based; u, v: dual variables4for (int i = 1; i <= n; i++) { // insert row i5p[0] = i;6int j0 = 0; // column 0 is a dummy that holds the new row7int[] minv = new int[n + 1];8boolean[] used = new boolean[n + 1];9Arrays.fill(minv, Integer.MAX_VALUE);10do {11used[j0] = true;12int i0 = p[j0], delta = Integer.MAX_VALUE, j1 = 0;13for (int j = 1; j <= n; j++) {14if (used[j]) continue;15int cur = cost[i0 - 1][j - 1] - u[i0] - v[j]; // reduced cost (slack) of edge i0 - j16if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }17if (minv[j] < delta) { delta = minv[j]; j1 = j; }18}19for (int j = 0; j <= n; j++) { // dual update: make the cheapest slack tight20if (used[j]) { u[p[j]] += delta; v[j] -= delta; }21else minv[j] -= delta;22}23j0 = j1;24} while (p[j0] != 0); // stop at a column nobody owns yet25do { int j1 = way[j0]; p[j0] = p[j1]; j0 = j1; } while (j0 != 0); // augment along the way[] chain26}27int[] result = new int[n];28for (int j = 1; j <= n; j++) result[p[j] - 1] = j - 1;29return result;30}
Worst
O(n³)
Space
O(n)
Hungarian algorithm: minimum-cost assignment with dual variables
Interactive · edit the inputrow 0 · u = 0
row 1 · u = 0
row 2 · u = 0
column potentials v
Each cell shows its reduced cost c − u[row] − v[col] with the original cost below it. The duals always keep every reduced cost ≥ 0; an edge with reduced cost 0 is tight. Rows are inserted one at a time; the assignment uses only tight edges.
1static int[] hungarian(int[][] cost) { // n x n costs; returns column assigned to each row2int n = cost.length;3int[] u = new int[n + 1], v = new int[n + 1], p = new int[n + 1], way = new int[n + 1]; // 1-based; u, v: dual variables4for (int i = 1; i <= n; i++) { // insert row i5p[0] = i;6int j0 = 0; // column 0 is a dummy that holds the new row7int[] minv = new int[n + 1];8boolean[] used = new boolean[n + 1];9Arrays.fill(minv, Integer.MAX_VALUE);10do {11used[j0] = true;12int i0 = p[j0], delta = Integer.MAX_VALUE, j1 = 0;13for (int j = 1; j <= n; j++) {14if (used[j]) continue;15int cur = cost[i0 - 1][j - 1] - u[i0] - v[j]; // reduced cost (slack) of edge i0 - j16if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }17if (minv[j] < delta) { delta = minv[j]; j1 = j; }18}19for (int j = 0; j <= n; j++) { // dual update: make the cheapest slack tight20if (used[j]) { u[p[j]] += delta; v[j] -= delta; }21else minv[j] -= delta;22}23j0 = j1;24} while (p[j0] != 0); // stop at a column nobody owns yet25do { int j1 = way[j0]; p[j0] = p[j1]; j0 = j1; } while (j0 != 0); // augment along the way[] chain26}27int[] result = new int[n];28for (int j = 1; j <= n; j++) result[p[j] - 1] = j - 1;29return result;30}
Live state
n
3
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=3 | Each cell shows its reduced cost c − u[row] − v[col] with the original cost below it. The duals always keep every reduced cost ≥ 0; an edge with reduced cost 0 is tight. Rows are inserted one at a time; the assignment uses only tight edges. |
| 2 | L5 | row=0 | Insert row 0: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached. |
| 3 | L23 | row=0 delta=1 column=1 owner=free | No tight edge leaves the tree. Smallest slack is 1: add it to u of the tree's rows and subtract it from v of the tree's columns. Now column 1 is tight and free. |
| 4 | L25 | row=0 assigned=0→1 | Augment: walk back along way[] and hand each column to the previous row. Row 0 is now assigned; earlier rows may have moved to other tight columns. |
| 5 | L5 | row=1 | Insert row 1: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached. |
| 6 | L23 | row=1 delta=0 column=1 owner=0 | A tight edge already reaches column 1 (slack 0, no dual change); it belongs to row 0, which joins the tree. |
| 7 | L23 | row=1 delta=2 column=0 owner=free | No tight edge leaves the tree. Smallest slack is 2: add it to u of the tree's rows and subtract it from v of the tree's columns. Now column 0 is tight and free. |
| 8 | L25 | row=1 assigned=1→0 0→1 | Augment: walk back along way[] and hand each column to the previous row. Row 1 is now assigned; earlier rows may have moved to other tight columns. |
| 9 | L5 | row=2 | Insert row 2: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached. |
| 10 | L23 | row=2 delta=2 column=2 owner=free | No tight edge leaves the tree. Smallest slack is 2: add it to u of the tree's rows and subtract it from v of the tree's columns. Now column 2 is tight and free. |
| 11 | L25 | row=2 assigned=1→0 0→1 2→2 | Augment: walk back along way[] and hand each column to the previous row. Row 2 is now assigned; earlier rows may have moved to other tight columns. |
| 12 | L29 | return=5 dual_sum=5 assignment=0→1 1→0 2→2 | Assignment 0→1, 1→0, 2→2 costs 5. The dual variables sum to 5: a feasible dual solution with the same value proves nothing cheaper exists. |
Cost matrix
The Hungarian routine wants a **square** matrix of costs to be **minimised**. Real problems rarely arrive that way, but small transformations fix every common variation. The code below pads a rectangular matrix with zero rows or columns and, when the goal is to maximise profit, replaces each value x by best − x so that maximising profit becomes minimising cost.
Padding with zeros means a dummy worker or a dummy job costs nothing, so the real assignment of min(rows, cols) pairs is chosen and the extra rows or columns absorb the leftovers. For a maximisation problem the padded cells have value 0, which becomes cost best; the optimal profit is then n · best − (minimum cost).
Forbidden pairs are modelled with a very large cost, larger than the sum of every real cost. If the optimal total reaches that magnitude, no valid assignment exists. Keep the numbers in int range, or use long, when you add large penalties.
| Situation | Transformation | Reading the answer |
|---|---|---|
| Minimise cost, square matrix | use costs as they are | total = sum of chosen cells |
| Maximise profit | cost = best − profit | profit = n · best − minimum cost |
| More workers than jobs (or the reverse) | pad with zero rows or columns | dummy partners mean “unassigned” |
| Pair not allowed | huge cost (for example 10^6) | a total above the huge value means infeasible |
| Costs can be negative | add a constant to every cell | subtract n times the constant from the total |
1// pads a rectangular matrix to a square one; maximize = true turns profits into costs (cost = best - profit)2static int[][] toSquareCost(int[][] value, boolean maximize) {3int rows = value.length, cols = value[0].length, n = Math.max(rows, cols), best = 0;4for (int[] r : value) for (int x : r) best = Math.max(best, x);5int[][] cost = new int[n][n];6for (int i = 0; i < n; i++)7for (int j = 0; j < n; j++) {8int x = i < rows && j < cols ? value[i][j] : 0; // a missing worker or job contributes nothing9cost[i][j] = maximize ? best - x : x;10}11return cost;12}
Dual variables
The assignment problem is a linear program: minimise Σ c[i][j] · x[i][j] with every row and column used exactly once. Its **dual** assigns a number u[i] to each row and v[j] to each column and maximises Σ u + Σ v subject to u[i] + v[j] ≤ c[i][j] for every cell. The inequality means the **reduced cost** c[i][j] − u[i] − v[j] is never negative.
The dual gives a cheap **lower bound**: every assignment picks one cell per row and per column, so its cost is at least Σ u + Σ v. If you can find an assignment whose cells are all **tight** (reduced cost 0), its cost equals the dual value, so it is optimal. This is **complementary slackness**, and the Hungarian algorithm is a systematic search for such a pair.
This is why the animation's final message mentions the dual sum: for the 3 × 3 example both the assignment cost and Σ u + Σ v are 5. The check below verifies feasibility and tightness directly, so an assignment together with duals is a **certificate** you can verify in O(n^2) without trusting the algorithm that produced it.
- Feasible duals always give a lower bound on the optimum.
- An assignment that only uses tight cells under feasible duals is optimal.
- A dual update adds
deltato the rows of the tree and subtracts it from the tree's columns, which keeps every cell feasible.
1// u[i] + v[j] <= cost[i][j] in every cell: a feasible dual. Its value is a lower bound on every assignment.2static boolean isFeasibleDual(int[][] cost, int[] u, int[] v) {3for (int i = 0; i < cost.length; i++)4for (int j = 0; j < cost.length; j++)5if (u[i] + v[j] > cost[i][j]) return false;6return true;7}8static int dualValue(int[] u, int[] v) {9int sum = 0;10for (int x : u) sum += x;11for (int x : v) sum += x;12return sum;13}14// row i takes column col[i]; optimal when the duals are feasible and every chosen cell is tight (complementary slackness)15static boolean isOptimalPair(int[][] cost, int[] col, int[] u, int[] v) {16if (!isFeasibleDual(cost, u, v)) return false;17for (int i = 0; i < col.length; i++) if (u[i] + v[col[i]] != cost[i][col[i]]) return false;18return true;19}
Perfect matching
The **equality subgraph** contains only the tight cells, those with u[i] + v[j] = c[i][j]. The Hungarian algorithm keeps a matching inside this subgraph at all times. If the equality subgraph has a **perfect matching**, that matching is an optimal assignment, by the certificate argument of the previous section.
When the equality subgraph has no perfect matching, the alternating search from a free row gets stuck in a tree. The tree's rows and columns form a bottleneck exactly as in Hall's theorem, and the dual update with delta makes at least one new edge tight at the boundary of this tree without breaking any existing tight edge. After at most n such updates per inserted row the search must succeed, which gives the O(n^3) total.
The code below tests the equality-subgraph condition with the Kuhn routine from the previous lessons: it tries to find a perfect matching using tight cells only. Note that when it returns true for duals produced by a finished Hungarian run, the matching it finds may differ from the one the algorithm returned. Both are optimal; ties in the matrix allow several optimal assignments, as the preset with all costs equal shows (every assignment costs 15).
| Condition | Meaning |
|---|---|
| Reduced cost ≥ 0 everywhere | duals are feasible |
| Equality subgraph has a perfect matching | that matching is optimal |
| No perfect matching and a stuck tree | update duals by delta: new tight edge appears |
| Duals sum = assignment cost | proof of optimality |
1static boolean tightPerfectMatching(int[][] cost, int[] u, int[] v) { // a perfect matching using only tight cells?2int n = cost.length;3int[] rowOf = new int[n];4Arrays.fill(rowOf, -1);5for (int i = 0; i < n; i++) if (!extend(i, cost, u, v, new boolean[n], rowOf)) return false;6return true;7}8static boolean extend(int i, int[][] cost, int[] u, int[] v, boolean[] seen, int[] rowOf) {9for (int j = 0; j < cost.length; j++) {10if (seen[j] || u[i] + v[j] != cost[i][j]) continue; // only tight cells are edges of the equality graph11seen[j] = true;12if (rowOf[j] == -1 || extend(rowOf[j], cost, u, v, seen, rowOf)) { rowOf[j] = i; return true; }13}14return false;15}
Practice ladder
Now try it yourself
Easy
- Assign Cookies
Medium
- Campus Bikes II
- Maximum Compatibility Score Sum
Advanced
- Distribute Repeating Integers
Key takeaways
- Hungarian: insert rows one at a time, grow a tree of tight edges, raise duals by the smallest slack when stuck. O(n^3).
- Model maximisation by cost = best − profit and unequal sizes by zero padding.
- Feasible duals plus an assignment on tight cells prove the assignment optimal.