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

MethodTime
bitmask DP over used jobs
Hungarian (Kuhn–Munkres, JV-style)–
min-cost flow (SSP)sparse graphs
auction, Gabow–Tarjan scalingtheory

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.

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