Back to Graph Theory Series

Hungarian Algorithm

August 30, 2026 Wasil Zafar 28 min read

The Hungarian algorithm turns a global search over $n!$ assignments into a cubic-time sequence of price adjustments and augmenting choices—while preserving a certificate that the final pairing is truly optimal.

Contents

  1. The Assignment Problem
  2. Prices and Tight Edges
  3. Two Views of the Algorithm
  4. Worked Matrix Example
  5. When Zeros Conflict
  6. Why It Is Correct
  7. Implementation
  8. Complexity
  9. Modeling Variants
  10. Applications
  11. Pitfalls & Checklist
  12. Exercises
  13. History & Takeaway

The Assignment Problem

Suppose worker $i$ can perform task $j$ at cost $c_{ij}$. We must choose exactly one task for every worker and exactly one worker for every task. If permutation $\pi$ records the chosen task for each worker, the optimization problem is

$$\min_{\pi}\ \sum_{i=1}^{n} c_{i,\pi(i)}.$$

Graphically, workers and tasks form the two sides of a weighted bipartite graph. A feasible assignment is a perfect matching; the goal is the perfect matching with minimum total edge weight. Brute force checks $n!$ permutations. The Hungarian algorithm reaches an optimum in $O(n^3)$ time.

A weighted bipartite assignment and its optimal matching Workers A, B, and C appear on the left and tasks 1, 2, and 3 on the right. All pairs are possible. The selected teal edges assign A to task 2 at cost 11, B to task 1 at cost 6, and C to task 3 at cost 6, totaling 23. Workers Tasks ABC 123 1166 11 + 6 + 6 = 23
The faded edges are alternatives; the teal edges are one globally optimal perfect matching. Choosing the cheapest edge for each worker independently would not enforce unique tasks.

Feasible

Every row and every column is used exactly once.

Objective

Minimize the sum of the selected matrix entries.

Certificate

Dual prices prove that no cheaper assignment exists.

Intuition: Prices and Tight Edges

Think of each row and column as carrying a price. After subtracting those prices, the remaining entry is a reduced cost: how much more expensive that pairing is than the current lower-bound explanation. A zero is a tight edge—a pairing whose original cost is fully explained by the current prices.

Market Analogy

Workers seek tasks with zero remaining premium. If several workers compete for too few tight tasks, the algorithm adjusts prices until a new zero appears. An alternating path then rearranges earlier choices and increases the matching by one without abandoning tight edges.

Zeros Are Opportunities, Not Assignments

A matrix may contain many zeros that collide in one row or column. The stopping condition is not “at least $n$ zeros”; it is “$n$ independent zeros,” one in every row and column.

Two Views of the Same Algorithm

The familiar matrix presentation is excellent for hand calculation:

  1. Row reduction: subtract the minimum of each row.
  2. Column reduction: subtract the minimum of each column.
  3. Zero graph: connect a row to a column wherever the reduced entry is zero, then find a maximum matching.
  4. Stop or expose: a matching of size $n$ is optimal. Otherwise, use a minimum vertex cover of the zero graph to mark rows and columns.
  5. Adjust: subtract the smallest uncovered entry from every uncovered cell and add it at every doubly covered intersection; repeat.

The implementation later uses the equivalent primal–dual shortest augmenting-path view. It grows the assignment one row at a time, maintains row/column potentials, stores each unused column's minimum slack, and updates only the values needed to reveal the next tight edge. This is the route to the compact $O(n^3)$ code.

Where Matching Theory Enters

Kőnig's theorem says the maximum number of independent zeros equals the minimum number of rows and columns covering every zero. Therefore fewer than $n$ cover lines precisely means the current zero graph lacks a perfect matching.

Worked Matrix Example

Use workers $A,B,C$ and tasks $1,2,3$ with the cost matrix below. Row reduction subtracts $9,6,6$. The second reduced column then has minimum $2$, so column reduction subtracts $2$ there.

Row and column reduction for a three by three assignment matrix The original matrix is transformed by subtracting row minima and then column minima. In the final matrix, selected teal zeros at A2, B1, and C3 form a complete independent assignment. An additional unselected zero at A1 is amber. Original costsAfter row reductionAfter column reduction 123 ABC 911146151312136 row minima: 9, 6, 6 123 ABC 025097670 column minima: 0, 2, 0 123 ABC 005077650 teal zeros: A→2, B→1, C→3
The final reduced matrix already has three independent zeros. No further adjustment is needed: $A\to2$, $B\to1$, $C\to3$ costs $11+6+6=23$ in the original matrix.

Why does a zero assignment recover the original cost? The reductions subtracted $9+6+6+2=23$ from every complete assignment. The selected entries total $0$ after reduction, so their original total is $0+23=23$. Checking the other five permutations confirms none is cheaper.

When Zeros Conflict

Reduction does not always reveal a perfect zero matching immediately. In the reduced matrix below, rows $A$ and $B$ can initially use only column $1$. The maximum independent-zero matching has size $2$, and all zeros can be covered by column $1$ plus row $C$.

123Zero options
$A$022$1$ only
$B$022$1$ only
$C$200$2$ or $3$

The smallest uncovered value is $\delta=2$. Subtract it from uncovered cells $(A,2),(A,3),(B,2),(B,3)$ and add it to the doubly covered intersection $(C,1)$. The new matrix is

$$\begin{bmatrix}0&0&0\\0&0&0\\4&0&0\end{bmatrix},$$

which admits independent zeros such as $A\to1$, $B\to2$, $C\to3$. The adjustment creates useful tight edges without invalidating the current dual prices.

Why the Method Is Correct

1. Reductions preserve the winner

Subtracting $r_i$ from row $i$ and $s_j$ from column $j$ subtracts the same total $\sum_i r_i+\sum_j s_j$ from every perfect assignment, because each assignment selects every row and column exactly once. Relative costs therefore do not change.

2. Potentials provide a lower bound

Choose row potentials $u_i$ and column potentials $v_j$ satisfying $u_i+v_j\le c_{ij}$. For any assignment $\pi$,

$$\sum_i c_{i,\pi(i)}\ \ge\ \sum_i\bigl(u_i+v_{\pi(i)}\bigr)=\sum_i u_i+\sum_j v_j.$$

The right-hand side is a lower bound on every feasible assignment. Reduced costs $c_{ij}-u_i-v_j$ are nonnegative; their zeros are exactly the tight edges.

3. A tight perfect matching closes the gap

If every selected edge is tight, its total cost equals the dual lower bound. A feasible solution that attains a universal lower bound must be optimal. The algorithm alternates between improving the matching on tight edges and adjusting potentials until such a perfect matching exists.

Primal–Dual Mental Model

The matching is the candidate solution (primal). The prices are the proof of how low any solution could possibly be (dual). Hungarian finishes when the candidate and proof have exactly the same value.

Implementation

def hungarian_algorithm(cost):
    """
    Minimize an n x n assignment matrix.
    Return (row_to_column, total_cost) using the primal-dual
    shortest-augmenting-path form of the Hungarian algorithm.
    """
    n = len(cost)
    if n == 0:
        return [], 0
    if any(len(row) != n for row in cost):
        raise ValueError("cost must be a square matrix")

    INF = float('inf')
    u = [0] * (n + 1)
    v = [0] * (n + 1)
    p = [0] * (n + 1)   # p[j] = row assigned to column j (1-indexed, 0 = unassigned)
    way = [0] * (n + 1)

    for i in range(1, n + 1):
        p[0] = i
        j0 = 0
        minv = [INF] * (n + 1)
        used = [False] * (n + 1)
        while True:
            used[j0] = True
            i0, delta, j1 = p[j0], INF, -1
            for j in range(1, n + 1):
                if not used[j]:
                    cur = cost[i0 - 1][j - 1] - u[i0] - v[j]
                    if cur < minv[j]:
                        minv[j] = cur
                        way[j] = j0
                    if minv[j] < delta:
                        delta, j1 = minv[j], j
            for j in range(n + 1):
                if used[j]:
                    u[p[j]] += delta
                    v[j] -= delta
                else:
                    minv[j] -= delta
            j0 = j1
            if p[j0] == 0:
                break
        while j0:
            j1 = way[j0]
            p[j0] = p[j1]
            j0 = j1

    assignment = [0] * n
    total_cost = 0
    for j in range(1, n + 1):
        if p[j] != 0:
            assignment[p[j] - 1] = j - 1
            total_cost += cost[p[j] - 1][j - 1]

    return assignment, total_cost

cost = [[9, 11, 14], [6, 15, 13], [12, 13, 6]]
assignment, total = hungarian_algorithm(cost)
print(assignment, total)   # [1, 0, 2]  23  -- worker0->task1, worker1->task0, worker2->task2
#include <vector>
#include <limits>
#include <iostream>
#include <stdexcept>
using namespace std;

pair<vector<int>, long long> hungarianAlgorithm(const vector<vector<long long>>& cost) {
    int n = cost.size();
    if (n == 0) return {{}, 0};
    for (const auto& row : cost)
        if ((int)row.size() != n) throw invalid_argument("cost must be square");
    const long long INF = numeric_limits<long long>::max() / 4;
    vector<long long> u(n + 1), v(n + 1);
    vector<int> p(n + 1), way(n + 1);

    for (int i = 1; i <= n; i++) {
        p[0] = i;
        int j0 = 0;
        vector<long long> minv(n + 1, INF);
        vector<bool> used(n + 1, false);
        do {
            used[j0] = true;
            int i0 = p[j0], j1 = -1;
            long long delta = INF;
            for (int j = 1; j <= n; j++) {
                if (!used[j]) {
                    long long cur = cost[i0 - 1][j - 1] - u[i0] - v[j];
                    if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }
                    if (minv[j] < delta) { delta = minv[j]; j1 = j; }
                }
            }
            for (int j = 0; j <= n; j++) {
                if (used[j]) { u[p[j]] += delta; v[j] -= delta; }
                else minv[j] -= delta;
            }
            j0 = j1;
        } while (p[j0] != 0);

        while (j0) {
            int j1 = way[j0];
            p[j0] = p[j1];
            j0 = j1;
        }
    }

    vector<int> assignment(n);
    long long totalCost = 0;
    for (int j = 1; j <= n; j++) {
        assignment[p[j] - 1] = j - 1;
        totalCost += cost[p[j] - 1][j - 1];
    }
    return {assignment, totalCost};
}

int main() {
    vector<vector<long long>> cost = {{9, 11, 14}, {6, 15, 13}, {12, 13, 6}};
    auto [assignment, total] = hungarianAlgorithm(cost);
    cout << "Total cost: " << total << endl;  // 23
    return 0;
}
import java.util.*;

class HungarianAlgorithm {
    static int[] solve(long[][] cost, long[] totalCostOut) {
        int n = cost.length;
        if (n == 0) return new int[0];
        for (long[] row : cost)
            if (row.length != n) throw new IllegalArgumentException("cost must be square");
        long[] u = new long[n + 1], v = new long[n + 1];
        int[] p = new int[n + 1], way = new int[n + 1];
        long INF = Long.MAX_VALUE / 4;

        for (int i = 1; i <= n; i++) {
            p[0] = i;
            int j0 = 0;
            long[] minv = new long[n + 1];
            boolean[] used = new boolean[n + 1];
            Arrays.fill(minv, INF);

            do {
                used[j0] = true;
                int i0 = p[j0], j1 = -1;
                long delta = INF;
                for (int j = 1; j <= n; j++) {
                    if (!used[j]) {
                        long cur = cost[i0 - 1][j - 1] - u[i0] - v[j];
                        if (cur < minv[j]) { minv[j] = cur; way[j] = j0; }
                        if (minv[j] < delta) { delta = minv[j]; j1 = j; }
                    }
                }
                for (int j = 0; j <= n; j++) {
                    if (used[j]) { u[p[j]] += delta; v[j] -= delta; }
                    else minv[j] -= delta;
                }
                j0 = j1;
            } while (p[j0] != 0);

            while (j0 != 0) {
                int j1 = way[j0];
                p[j0] = p[j1];
                j0 = j1;
            }
        }

        int[] assignment = new int[n];
        long totalCost = 0;
        for (int j = 1; j <= n; j++) {
            assignment[p[j] - 1] = j - 1;
            totalCost += cost[p[j] - 1][j - 1];
        }
        totalCostOut[0] = totalCost;
        return assignment;
    }

    public static void main(String[] args) {
        long[][] cost = {{9, 11, 14}, {6, 15, 13}, {12, 13, 6}};
        long[] totalCost = new long[1];
        int[] assignment = solve(cost, totalCost);
        System.out.println("Total cost: " + totalCost[0]);  // 23
    }
}

The arrays mirror the proof:

  • u and v are row and column potentials.
  • minv[j] is the smallest known reduced-cost slack for reaching unused column j.
  • way[j] remembers the alternating-path predecessor used to reconstruct an augmentation.
  • p[j] stores the row currently matched to column j; column 0 is a sentinel that anchors the search.

Complexity

For each of $n$ rows, the augmenting search can visit $O(n)$ columns, and each visit scans up to $O(n)$ unused columns. Therefore:

$$\text{time}=O(n^3),\qquad \text{auxiliary space}=O(n),\qquad \text{input space}=O(n^2).$$

The classroom cover-and-adjust matrix presentation can take $O(n^4)$ when its matching and covering work is implemented naively. The shortest-augmenting-path potential formulation in the code avoids rescanning the full matrix after every small adjustment and achieves the standard cubic bound.

Rectangular, Maximization, and Forbidden Assignments

SituationTransformationImportant interpretation
$r\ne c$pad to $N=\max(r,c)$ with dummy rows or columnsa dummy match means “unassigned”
maximize value $w_{ij}$use $c_{ij}=M-w_{ij}$, or negate values if the implementation supports negativeschoose one common $M$; it adds the same constant to every complete assignment
optional assignmentsset dummy-edge costs to the desired non-assignment penaltyzero is correct only when leaving an item unmatched is truly free
forbidden pairuse a carefully bounded large penalty, then reject any result that selects itthe sentinel must not overflow arithmetic or masquerade as feasible
floating-point costsuse tolerances when testing reduced costs for zeroexact equality is brittle after many updates

Padding Changes the Model

Dummy entries are decisions, not bookkeeping. Their costs encode the business meaning of leaving a worker or task unmatched. Pick those penalties before running the algorithm, then remove dummy matches from the reported result.

Where Assignment Modeling Appears

Case Study

Multi-Object Tracking in Computer Vision

A tracker can place existing tracks in the rows and new detections in the columns. Each cost combines motion disagreement, geometric overlap, and appearance dissimilarity. Solving the assignment chooses a globally consistent one-to-one association for that frame; dummy entries represent births, missed detections, or retired tracks.

Object TrackingComputer Vision
  • Workforce scheduling: assign people to shifts or jobs using travel time, preference, or qualification penalties.
  • Resource allocation: map machines to jobs, vehicles to requests, or channels to users.
  • Evaluation and label alignment: optimally align predicted clusters, components, or identities with reference labels.
  • Data association: match observations across images, time steps, or sensor streams.

The one-to-one, additive objective is the key. If a task needs several workers, assignments interact through time, or costs depend on combinations of pairs, the model has moved beyond the classical linear assignment problem.

Common Pitfalls and a Reliable Checklist

PitfallConsequenceReliable practice
choosing each row minimum greedilyseveral rows may claim one columnmaintain a global matching
counting zeros instead of matching themcolliding zeros look falsely completeseek $n$ independent zeros
subtracting only uncovered cellsbreaks the reduction invariantalso add $\delta$ at doubly covered intersections
padding with arbitrary zeroesunmatched items may become artificially attractiveencode the real non-assignment penalty
using a huge sentinel near the integer limitpotential arithmetic can overflowderive a safe bound and validate the final assignment
assuming the optimum is uniquetests reject a different valid optimumverify feasibility and total cost, not one exact permutation

Before You Run It

Confirm the objective is additive and one-to-one; define dummy and forbidden costs; choose integer widths or floating tolerances; validate matrix dimensions; and decide how ties should be reported.

Exercises

  1. Trace: reproduce both reductions in the SVG and list all six original assignment totals.
  2. Conflict: for the adjustment matrix, find a minimum line cover, apply $\delta=2$, and give two different perfect zero matchings in the result.
  3. Certificate: recover row and column potentials for the worked example and verify that each selected edge is tight.
  4. Maximization: transform rewards $[[8,7,6],[6,9,5],[7,8,9]]$ into costs and confirm the best total reward.
  5. Rectangular case: model three workers and two tasks where leaving a worker unassigned costs $4$. Explain the dummy column before solving.
  6. Testing: compare the Python output against brute force on random matrices up to $n=7$, checking the total rather than the exact assignment.

History and Takeaway

Harold Kuhn published the Hungarian method in 1955, naming it for the foundational work of Hungarian mathematicians Dénes Kőnig and Jenő Egerváry. James Munkres studied assignment and transportation algorithms in 1957, helping establish the method's strongly polynomial character. The original matrix-style method is commonly analyzed at $O(n^4)$; later augmenting-path refinements produced the $O(n^3)$ form implemented above.

The Mental Model to Keep

Hungarian grows a one-to-one matching while adjusting row and column prices. Those prices keep every reduced cost nonnegative; zero reduced-cost edges are currently justified choices. Once a perfect matching uses only justified edges, its cost meets the price-based lower bound, so optimality is proven—not guessed.