Matching two sets: the assignment problem
Read these first if they are new to you
The idea in one paragraph
You have two sets of the same size, say three red points and three blue points, and a cost for every red–blue pair, such as the distance between them. The assignment problem asks for the pairing, each red with exactly one blue, that makes the total cost as small as possible. Grabbing the cheapest pair first can go badly wrong. Trying every pairing works for small sets but explodes as they grow. The Hungarian algorithm, in its faster modern form, finds the best pairing in about n³ steps, and its key trick fits in one worked example.
A pairing picks one cell in every row and every column of the cost table
Put the costs in a table, the cost matrix: one row per red point, one column per blue point. Each cell holds the distance for that pair, rounded here to one decimal. An assignment picks one cell in each row with no two cells in the same column, so every point gets exactly one partner. Its total is the sum of the picked cells.
Click one cell in each row. A click takes that column away from any other row. Find the lowest total you can before you press "Show the best".
| B1 | B2 | B3 |
|---|
Taking the cheapest pair first can force an expensive pair later
A natural shortcut is greedy matching: take the cheapest pair in the table, remove its row and column, and repeat. Here it takes R2–B1 (1.0), then R3–B2 (2.0). That leaves R1 with only B3, at 6.7. The total is 9.7.
The best pairing gives up R3's cheap 2.0 and pairs R1 with B2 instead: R1–B2 (2.8), R2–B1 (1.0) and R3–B3 (2.2), a total of 6.0. Greedy fails because one cheap choice can leave a far worse one for someone else.
Subtracting a number from a whole row does not change which pairing is best
Every pairing uses exactly one cell from each row. So if you subtract 2.8 from every cell in row R1, every pairing's total drops by exactly 2.8. Their order stays the same, and so does the best one. The same holds for columns. This is the main trick of the Hungarian algorithm: subtract until zeros appear, then look for a pairing made only of zeros.
- Subtract each row's smallest cell: 2.8 from R1, 1.0 from R2 and 2.0 from R3.
- Now every column has a zero except B3, whose smallest cell is 0.2. Subtract 0.2 from column B3.
- Find a pairing that uses only zeros: R1–B2, R2–B1, R3–B3.
- Total subtracted: 2.8 + 1.0 + 2.0 + 0.2 = 6.0. Every cell is now 0 or more, so no pairing can cost less than 6.0 in the original table. The zero pairing costs exactly 6.0, so it is the best.
| B1 | B2 | B3 | |
|---|---|---|---|
| R1 | 1.3 | 0 | 3.7 |
| R2 | 0 | 0.4 | 3.8 |
| R3 | 1.6 | 0 | 0 |
Here the first subtractions already gave a pairing of zeros. Often they do not. The Hungarian algorithm then covers all zeros with as few lines as possible, finds the smallest uncovered number, and moves it around the table in a way that keeps every cell at 0 or more. It repeats until a full pairing of zeros exists. That part takes more than one page to explain well; see Go further.
Trying every pairing stops working past about ten points
For n points there are n! pairings ("n factorial", 1 × 2 × … × n): n choices for the first red point, n − 1 for the second, and so on. The Hungarian algorithm, in its faster modern form, takes about n³ steps instead. The line-covering version in the box above is slower, about n⁴ steps at worst.
| n | Pairings, n! | About n³ |
|---|---|---|
| 3 | 6 | 27 |
| 10 | 3,628,800 | 1,000 |
| 20 | about 2.4 × 1018 | 8,000 |
| 1,000 | a number with 2,568 digits | 109 |
The code below checks all six pairings of the example. For real work, use a library function such as SciPy's
scipy.optimize.linear_sum_assignment, which takes the cost matrix and returns the best pairing.
assign.py
import itertools
import numpy as np
cost = np.array([[4.1, 2.8, 6.7], # row i: red point i
[1.0, 1.4, 5.0], # column j: blue point j
[3.6, 2.0, 2.2]])
n = len(cost)
best_total, best_pairs = None, None
for pairs in itertools.permutations(range(n)): # every way to pair them up
total = 0.0
for i in range(n):
total += cost[i, pairs[i]] # red i goes to blue pairs[i]
if best_total is None or total < best_total: # the first pairing, or a lower total
best_total, best_pairs = total, pairs
print(best_pairs, round(best_total, 1))
(1, 0, 2) 6.0
itertools.permutations(range(3)) lists every ordering of 0, 1 and 2, six in all. The answer
(1, 0, 2) means red 0 goes to blue 1, red 1 to blue 0, and red 2 to blue 2: R1–B2, R2–B1, R3–B3.
The two sets need not be the same size. With more blue points than red, each red point still gets one partner and some blue points get none. Library functions accept such rectangular tables.
Check yourself
-
Your turn: use row and column subtraction to find the best pairing for this table, and its total.
B1 B2 B3 R1 3 7 5 R2 2 4 6 R3 8 5 9 Answer
Row minimums 3, 2 and 5 give rows (0, 4, 2), (0, 2, 4) and (3, 0, 4). Column B3's smallest is then 2; subtract it: (0, 4, 0), (0, 2, 2), (3, 0, 2). A pairing of zeros: R1–B3, R2–B1, R3–B2. Its cost is 5 + 2 + 5 = 12, which equals the total subtracted, 3 + 2 + 5 + 2.
-
How many pairings would brute force check for 5 red and 5 blue points? For 12 and 12?
Answer
5! = 120. 12! = 479,001,600, almost half a billion.
-
In the first table, greedy took R3–B2 at 2.0. Why does the best pairing skip that cheap cell?
Answer
B2 is also R1's only nearby partner. If R3 takes B2, R1 must take B3 at 6.7. Giving B2 to R1 and B3 to R3 costs 2.8 + 2.2 = 5.0 for those two, against 2.0 + 6.7 = 8.7.
Go further
Optional background.
-
The full algorithm on a cost table, step by step, including the line-covering step this page skips.
-
The function to call in practice, with a short example and notes on rectangular tables.
-
Why the method is correct and how to make it run in n³ steps, with code.
Where this shows up
No unit or explanation builds on this one yet.