Chapter 40: Graph Matchings and Assignment
40.04Intermediate / Advanced4 topics20 min1 interactive animation

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

Bipartite matching (Lesson 40.02)Shortest paths with non-negative weights (Chapter 35)

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.

40.04.1

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.

Java
1static int[] hungarian(int[][] cost) { // n x n costs; returns column assigned to each row
2 int n = cost.length;
3 int[] 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 variables
4 for (int i = 1; i <= n; i++) { // insert row i
5 p[0] = i;
6 int j0 = 0; // column 0 is a dummy that holds the new row
7 int[] minv = new int[n + 1];
8 boolean[] used = new boolean[n + 1];
9 Arrays.fill(minv, Integer.MAX_VALUE);
10 do {
11 used[j0] = true;
12 int i0 = p[j0], delta = Integer.MAX_VALUE, j1 = 0;
13 for (int j = 1; j <= n; j++) {
14 if (used[j]) continue;
15 int cur = cost[i0 - 1][j - 1] - u[i0] - v[j]; // reduced cost (slack) of edge i0 - j
16 if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }
17 if (minv[j] < delta) { delta = minv[j]; j1 = j; }
18 }
19 for (int j = 0; j <= n; j++) { // dual update: make the cheapest slack tight
20 if (used[j]) { u[p[j]] += delta; v[j] -= delta; }
21 else minv[j] -= delta;
22 }
23 j0 = j1;
24 } while (p[j0] != 0); // stop at a column nobody owns yet
25 do { int j1 = way[j0]; p[j0] = p[j1]; j0 = j1; } while (j0 != 0); // augment along the way[] chain
26 }
27 int[] result = new int[n];
28 for (int j = 1; j <= n; j++) result[p[j] - 1] = j - 1;
29 return result;
30}

Worst

O(n³)

Space

O(n)

Hungarian algorithm: minimum-cost assignment with dual variables

Interactive · edit the input

Your input

Try

row 0 · u = 0

4cost 4
1cost 1
3cost 3

row 1 · u = 0

2cost 2
0cost 0
5cost 5

row 2 · u = 0

3cost 3
2cost 2
2cost 2

column potentials v

0col 0
0col 1
0col 2
Setup

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.

Step 1 / 12
Java
1static int[] hungarian(int[][] cost) { // n x n costs; returns column assigned to each row
2 int n = cost.length;
3 int[] 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 variables
4 for (int i = 1; i <= n; i++) { // insert row i
5 p[0] = i;
6 int j0 = 0; // column 0 is a dummy that holds the new row
7 int[] minv = new int[n + 1];
8 boolean[] used = new boolean[n + 1];
9 Arrays.fill(minv, Integer.MAX_VALUE);
10 do {
11 used[j0] = true;
12 int i0 = p[j0], delta = Integer.MAX_VALUE, j1 = 0;
13 for (int j = 1; j <= n; j++) {
14 if (used[j]) continue;
15 int cur = cost[i0 - 1][j - 1] - u[i0] - v[j]; // reduced cost (slack) of edge i0 - j
16 if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }
17 if (minv[j] < delta) { delta = minv[j]; j1 = j; }
18 }
19 for (int j = 0; j <= n; j++) { // dual update: make the cheapest slack tight
20 if (used[j]) { u[p[j]] += delta; v[j] -= delta; }
21 else minv[j] -= delta;
22 }
23 j0 = j1;
24 } while (p[j0] != 0); // stop at a column nobody owns yet
25 do { int j1 = way[j0]; p[j0] = p[j1]; j0 = j1; } while (j0 != 0); // augment along the way[] chain
26 }
27 int[] result = new int[n];
28 for (int j = 1; j <= n; j++) result[p[j] - 1] = j - 1;
29 return result;
30}

Live state

0 dual updates0 rows inserted

n

3

No condition evaluated on this step
Execution trace (dry run table)Show 12 steps
StepLineStateWhat happens
1L1n=3Each 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.
2L5row=0Insert row 0: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached.
3L23row=0 delta=1 column=1 owner=freeNo 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.
4L25row=0 assigned=0→1Augment: 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.
5L5row=1Insert row 1: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached.
6L23row=1 delta=0 column=1 owner=0A tight edge already reaches column 1 (slack 0, no dual change); it belongs to row 0, which joins the tree.
7L23row=1 delta=2 column=0 owner=freeNo 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.
8L25row=1 assigned=1→0 0→1Augment: 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.
9L5row=2Insert row 2: grow an alternating tree of tight edges from it until some column that is free (or can be freed) is reached.
10L23row=2 delta=2 column=2 owner=freeNo 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.
11L25row=2 assigned=1→0 0→1 2→2Augment: 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.
12L29return=5 dual_sum=5 assignment=0→1 1→0 2→2Assignment 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.
40.04.2

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.

SituationTransformationReading the answer
Minimise cost, square matrixuse costs as they aretotal = sum of chosen cells
Maximise profitcost = best − profitprofit = n · best − minimum cost
More workers than jobs (or the reverse)pad with zero rows or columnsdummy partners mean “unassigned”
Pair not allowedhuge cost (for example 10^6)a total above the huge value means infeasible
Costs can be negativeadd a constant to every cellsubtract n times the constant from the total
Java
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) {
3 int rows = value.length, cols = value[0].length, n = Math.max(rows, cols), best = 0;
4 for (int[] r : value) for (int x : r) best = Math.max(best, x);
5 int[][] cost = new int[n][n];
6 for (int i = 0; i < n; i++)
7 for (int j = 0; j < n; j++) {
8 int x = i < rows && j < cols ? value[i][j] : 0; // a missing worker or job contributes nothing
9 cost[i][j] = maximize ? best - x : x;
10 }
11 return cost;
12}
40.04.3

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 delta to the rows of the tree and subtracts it from the tree's columns, which keeps every cell feasible.
Java
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) {
3 for (int i = 0; i < cost.length; i++)
4 for (int j = 0; j < cost.length; j++)
5 if (u[i] + v[j] > cost[i][j]) return false;
6 return true;
7}
8static int dualValue(int[] u, int[] v) {
9 int sum = 0;
10 for (int x : u) sum += x;
11 for (int x : v) sum += x;
12 return 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) {
16 if (!isFeasibleDual(cost, u, v)) return false;
17 for (int i = 0; i < col.length; i++) if (u[i] + v[col[i]] != cost[i][col[i]]) return false;
18 return true;
19}
40.04.4

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).

ConditionMeaning
Reduced cost ≥ 0 everywhereduals are feasible
Equality subgraph has a perfect matchingthat matching is optimal
No perfect matching and a stuck treeupdate duals by delta: new tight edge appears
Duals sum = assignment costproof of optimality
Java
1static boolean tightPerfectMatching(int[][] cost, int[] u, int[] v) { // a perfect matching using only tight cells?
2 int n = cost.length;
3 int[] rowOf = new int[n];
4 Arrays.fill(rowOf, -1);
5 for (int i = 0; i < n; i++) if (!extend(i, cost, u, v, new boolean[n], rowOf)) return false;
6 return true;
7}
8static boolean extend(int i, int[][] cost, int[] u, int[] v, boolean[] seen, int[] rowOf) {
9 for (int j = 0; j < cost.length; j++) {
10 if (seen[j] || u[i] + v[j] != cost[i][j]) continue; // only tight cells are edges of the equality graph
11 seen[j] = true;
12 if (rowOf[j] == -1 || extend(rowOf[j], cost, u, v, seen, rowOf)) { rowOf[j] = i; return true; }
13 }
14 return 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.