Cairn
algorithms · network flow · O(V²E)

back to Network Flow

Push-Relabel Algorithm

Edmonds-Karp and Dinic's algorithm both belong to the same family: search the whole residual graph for a path from source to sink, push flow along it, repeat. Every intermediate state along the way is a genuine, conservation- respecting flow. Push-relabel (Goldberg-Tarjan) throws that family's whole strategy away. It never searches for a path at all — instead it lets flow overflow at individual nodes on purpose, then fixes each overflow with a strictly local decision: does this node have a neighbor it's allowed to send its extra flow to right now? If yes, push some. If no, raise this node's own height just enough to create one, and try again. No node ever needs to know what's happening three hops away.

Formally: every node other than the source S and sink T gets a height h(v), all starting at 0 except h(S) = V (fixed) and h(T) = 0 (fixed, and never changes). A node also has an excess — inflow minus outflow, normally required to be zero everywhere but S and T for something to count as a real flow, but allowed to sit positive here mid-algorithm; a state with positive excess elsewhere is called a preflow, not a flow. A residual edge u→v is admissible only when h(u) = h(v) + 1 — exactly one step downhill. Every active node (excess > 0, not S or T) repeats one of two moves: push as much excess as the residual edge allows across any admissible edge it has, or, if it has none, relabel — raise its own height to 1 + min over the heights of every node it still has residual capacity toward, the smallest change that creates a new admissible edge. The algorithm stops when no node but S/T has excess left. The demo below reuses Edmonds-Karp's and Dinic's exact graph, so all three pages can be compared directly.

Why this finds a maximum flow without ever running a global search

Two facts about heights, both easy to check locally, are what let push-relabel skip the "search the whole graph" step Edmonds-Karp and Dinic's both depend on. First, heights only ever go up — a relabel is the only thing that changes a node's height, and it always increases it. Second, the algorithm never lets the invariant h(u) ≤ h(v) + 1 break for any residual edge u→v: a push only ever fires on an edge where that already holds with equality, and it can only shrink or reverse that same edge's residual capacity, never any other edge out of u; a relabel exists specifically to restore the invariant for every residual edge out of the node being relabeled, all at once.

That invariant is the whole proof. Suppose, at any point, a path of residual edges existed from S to T — length k, at most V − 1 since a simple path can't repeat a node. Chaining the invariant across every edge on that path gives h(S) ≤ h(T) + k. But h(S) = V and h(T) = 0 are both fixed for the algorithm's entire run, so this would require V ≤ k ≤ V − 1 — a contradiction. No such path can ever exist, at any point during execution, not just at the end. So when the algorithm finally halts (no active vertex remains, the preflow has become a real flow), that flow is already known to have no augmenting path in its residual graph — the exact condition Edmonds-Karp and Dinic's each run one final BFS to confirm. Push-relabel gets that guarantee for free, baked into an invariant it maintains at every single step, rather than checking for it once at the very end.

Try it

Same six nodes and eight directed edges as the Edmonds-Karp and Dinic's demos, labeled flow/capacity. Press Step or Run. Every node shows two small badges: a height (bottom-right, always visible — this is h(v)) and an excess (top-left, only visible while positive — this is the "overflow" waiting to be pushed or relabeled away). The currently active node is outlined; a push highlights the admissible edge it uses, a relabel just updates that node's height badge. Preflow initialization saturates every edge leaving S before the main loop even starts — watch A and B pick up excess immediately, before either has done anything itself.

pushes: 0 · relabels: 0 · flow so far: 0
Press Step or Run.

Reference implementation

Matches the demo above one for one — same generic "pick any active vertex" selection rule, same push/relabel logic — just without the yield points the demo uses to show every individual operation:

function pushRelabel(numNodes, edges, s, t) {
  const cap = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
  edges.forEach(e => { cap[e.a][e.b] += e.cap; });
  const flow = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
  const h = new Array(numNodes).fill(0);
  h[s] = numNodes;
  const excess = new Array(numNodes).fill(0);

  // Preflow init: saturate every edge leaving the source.
  edges.forEach(e => {
    if (e.a === s) {
      flow[s][e.b] += e.cap;
      flow[e.b][s] -= e.cap;
      excess[e.b] += e.cap;
      excess[s] -= e.cap;
    }
  });

  function activeVertex() {
    for (let v = 0; v < numNodes; v++) {
      if (v !== s && v !== t && excess[v] > 0) return v;
    }
    return -1;
  }

  let u;
  while ((u = activeVertex()) !== -1) {
    let pushed = false;
    for (let v = 0; v < numNodes; v++) {
      const residual = cap[u][v] - flow[u][v];
      if (residual > 0 && h[u] === h[v] + 1) {
        const amount = Math.min(excess[u], residual);
        flow[u][v] += amount;
        flow[v][u] -= amount;
        excess[u] -= amount;
        excess[v] += amount;
        pushed = true;
        break; // one push per iteration, same granularity the demo shows
      }
    }
    if (pushed) continue;

    let minHeight = Infinity;
    for (let v = 0; v < numNodes; v++) {
      if (cap[u][v] - flow[u][v] > 0) minHeight = Math.min(minHeight, h[v]);
    }
    h[u] = minHeight + 1; // smallest raise that creates a new admissible edge
  }

  return excess[t]; // no active vertices left: preflow is now a maximum flow
}

Pitfalls

Dropping the height admissibility check doesn't slow the algorithm down — it breaks it outright, immediately. A version that pushes an active node's excess across the first residual edge with any spare capacity, ignoring h(u) = h(v) + 1 entirely, was run against this page's own demo graph: it halts after just 4 pushes and 0 relabels at a max flow of 0, not the true 15. What happens is exactly what the check exists to prevent: after the preflow init pushes S→A and S→B (2 of those 4 pushes), both A and B still have a residual edge straight back to S — the reverse of the edge that just gave them their excess, and the first residual edge index order finds for each. Without the height rule to forbid it, each node's very next move is to hand its entire excess right back where it came from (the other 2 pushes), and the "algorithm" halts having moved nothing anywhere. The height invariant isn't an optimization; it's what forces excess to flow generally downhill, toward the sink, instead of sloshing back uphill to the source the instant it's offered the chance.

Relabeling to the maximum neighbor height instead of the minimum breaks correctness the same way, just with two relabels in between instead of zero. Swapping h[u] = 1 + min(...) for h[u] = 1 + max(...) — a plausible typo, since both read as "pick a neighbor height and add one" — was also run against the demo graph: 4 pushes and 2 relabels, max flow 0 again. A's residual neighbors after preflow init are S (height 6, via the reverse edge), B, and C (both height 0); taking the max relabels A straight to height 7 in one move — high enough to push its entire excess back to S immediately, the same washback failure as above, just reached via one extra relabel instead of skipping the check completely. B does the same thing right after. The minimum is what makes a relabel the smallest possible change: it opens exactly one new admissible edge, toward the closest-to-T neighbor available, rather than leaping straight to whichever neighbor happens to be closest to the source.

A preflow is not a flow, and reading intermediate state as if it were gives a wrong number. Unlike Edmonds-Karp or Dinic's, where every step in the demo represents a real, conservation-respecting flow you could stop and report, push-relabel's intermediate states genuinely violate conservation — after just the first preflow-init step here, excess(A) = 10, meaning 10 more flow enters A than leaves it, which is not a valid flow by definition. Summing "flow received at T so far" mid-run (what the demo's stats bar shows) is meaningful as a running lower bound, but the algorithm's actual answer — a value provably equal to the true max flow — only exists once every node but S and T reaches zero excess. Stopping early and reporting excess(T) as "the answer" isn't a rounding error, it's reporting a number that isn't the max flow, isn't a flow's value at all, and can be arbitrarily far from 15 depending on when you stopped.

Complexity

Time: O(V²E) for the generic algorithm — the version implemented above and in the demo, which picks any active vertex with no particular rule. The bound comes from two pieces: relabels are capped at O(V) per node (a node's height can rise to at most 2V − 1 before it's provably able to route all its excess back toward the source) for O(V²) total, saturating pushes (the residual edge empties completely) are capped at O(VE) since each one needs another relabel before that same edge can be pushed again, and non-saturating pushes (excess runs out before the edge does) are bounded by O(V²E) via a potential-function argument — the term that dominates the total. Smarter active-vertex selection rules tighten this without changing the algorithm's actual operations: FIFO order (always work the longest-waiting active vertex) gets O(V³); always picking the highest-labeled active vertex gets O(V²√E), the standard textbook bound and the one usually meant by "push-relabel" without further qualification. Space: O(V²) for the capacity/flow matrices as implemented here (or O(V + E) with adjacency lists), plus O(V) each for the height and excess arrays.

See Choosing a Network Flow Algorithm for how this page's local push/relabel model compares against Edmonds-Karp's and Dinic's global searches side by side, and when that locality is actually the deciding factor rather than raw complexity.