Assign workers to distinct jobs among minimising : a minimum-cost perfect matching in a complete bipartite graph. It hides behind “pick one entry per row in distinct columns”, “pair red and blue points minimising total distance”, “schedule tasks to machines with costs”. Greedy is wrong (\begin{psmallmatrix}1&2\\4&100\end{psmallmatrix}: greedy 101, optimum 6).
| Method | Time | |
|---|---|---|
| bitmask DP over used jobs | ||
| Hungarian (Kuhn–Munkres, JV-style) | – | |
| min-cost flow (SSP) | sparse graphs | |
| auction, Gabow–Tarjan scaling | theory |
Hungarian algorithm
Maintain potentials with and add rows one at a time: each row runs a Dijkstra on reduced costs over columns, shifting potentials by the minimum slack until an unmatched column is reached, then flips the alternating path.
pair<ll, vector<int>> hungarian(const vector<vector<ll>>& a) { // n rows <= m columns; {min cost, row -> column}
if (a.empty()) return {0, {}};
int n = sz(a) + 1, m = sz(a[0]) + 1;
vector<ll> u(n), v(m); vector<int> p(m), ans(n - 1); // p[j]: row matched to column j (0 = none)
for (int i = 1; i < n; i++) {
p[0] = i; int j0 = 0; // column 0 is a dummy holding the new row
vector<ll> dist(m, LLONG_MAX); vector<int> pre(m, -1); vector<char> done(m + 1);
do {
done[j0] = 1; int i0 = p[j0], j1 = 0; ll delta = LLONG_MAX;
for (int j = 1; j < m; j++) if (!done[j]) {
ll cur = a[i0 - 1][j - 1] - u[i0] - v[j];
if (cur < dist[j]) dist[j] = cur, pre[j] = j0;
if (dist[j] < delta) delta = dist[j], j1 = j;
}
for (int j = 0; j < m; j++) if (done[j]) u[p[j]] += delta, v[j] -= delta; else dist[j] -= delta;
j0 = j1;
} while (p[j0]);
while (j0) { int j1 = pre[j0]; p[j0] = p[j1], j0 = j1; }
}
for (int j = 1; j < m; j++) if (p[j]) ans[p[j] - 1] = j - 1;
return {-v[0], ans};
}- Maximise: negate the matrix. Forbidden pairs: a large cost (check the answer against it).
- Maximum-weight matching, not necessarily perfect, with : pad to a square matrix with 0 for absent pairs and maximise.
- The potentials are an optimality certificate: equals the answer, and the same dual-potential idea reappears in min-cost flow’s successive-shortest-paths and in Johnson’s reweighting — understanding it once pays off repeatedly.
- best assignments: Murty’s partitioning (fix / forbid one pair at a time from the best solution), Hungarian runs per output.
Proof: Hungarian is optimal and
Dual feasibility is maintained for the rows inserted so far (negative costs are fine). A new row starts with ; the first step of its phase has only the dummy column in the tree, computes (possibly negative) and shifts by , after which all of row ‘s constraints hold with the minimiser tight. From then on every slack is , so . Updates: for tree rows and tree columns both shifted, is unchanged; tree row with non-tree column loses exactly its slack ( is the minimum slack
dist); non-tree row with tree column gains slack. So feasibility is kept and the edge achieving becomes tight. Matched edges stay tight. Each step adds one column to the tree, so a phase has steps of work, and phases give .Optimality: columns enter the tree only during a phase, and every tree column is matched at its end, so columns with are matched and always (columns only decrease). At the end, matched edges are tight: cost . For any assignment , (the extra only lower the right side). So the matching is optimal. The returned value: every step raises by exactly (one more tree row than tree columns , because of the dummy column 0) and lowers by , so equals the dual objective, which is the cost.
Small : bitmask DP
ll assignmentDP(const vector<vector<ll>>& c) { // n rows <= m columns, m <= ~20
int n = sz(c), m = sz(c[0]); vector<ll> dp(1 << m, LLONG_MAX); dp[0] = 0; ll best = LLONG_MAX;
for (int mask = 0; mask < (1 << m); mask++) {
if (dp[mask] == LLONG_MAX) continue;
int i = __builtin_popcount(mask); // the next row to assign
if (i == n) { best = min(best, dp[mask]); continue; }
for (int j = 0; j < m; j++) if (!(mask >> j & 1)) dp[mask | 1 << j] = min(dp[mask | 1 << j], dp[mask] + c[i][j]);
}
return best;
}Special structure
- Monge / convex costs on a line: if with convex (e.g. , ), sorting both sides and matching in order is optimal; with it becomes a DP over sorted lists in (or slope trick).
- Bottleneck assignment (minimise the maximum cost): binary search a threshold and test for a perfect matching with Hopcroft–Karp.
- Points in the plane, Euclidean costs: still Hungarian in general; Manhattan distance on a line is the sorted case.
- 3-dimensional assignment is NP-hard.
- Workers can take up to jobs each (generalised/transportation assignment, unequal supply and demand): drop Hungarian for min-cost flow with capacities on the worker side.
- Costs change between queries: either rerun Hungarian from scratch, or maintain the dual potentials and re-augment incrementally from the changed rows/columns only.
- Non-bipartite (match within one set, not between two): this is weighted general matching, not assignment — see General Matching (weighted Blossom).
Proof: sorted matching for convex
For and , put both and , and . Convexity of gives (the pair closer to the middle has the smaller sum). So un-crossing any inverted pair of an assignment does not increase the cost, and repeatedly doing so reaches the sorted assignment.