DSA DSA In Depth
0%
lesson push-relabelgoldberg-tarjanmax-flow

Push-Relabel (Goldberg-Tarjan): Preflows, Height Functions & Highest-Label Selection

7 min read Module 5.3: Network Flow, Min-Cut & Bipartite Matching Updated Oct 23, 2026
📄 Cheat Sheet
The Push-Relabel Algorithm for Maximum Flow — Preflows & Height Labels
Tim Roughgarden Lectures The Push-Relabel Algorithm for Maximum Flow — Preflows & Height Labels Watch on YouTube ↗

Explains how Goldberg and Tarjan localized network flow by relaxing conservation into preflows and guiding water downhill via vertex heights.

0:00 / --:--

Preflows, Valid Height Functions & Local Push/Relabel Operations

A preflow $f$ satisfies capacity constraints ($0 \le f(u,v) \le c(u,v)$) and relaxed conservation: for every $u \in V \setminus \{s\}$, the excess flow is non-negative:

$$e(u) = \sum_{v \in V} f(v, u) - \sum_{w \in V} f(u, w) \ge 0$$

A vertex $u \in V \setminus \{s, t\}$ with $e(u) > 0$ is called active (overflowing).

Formal Invariant — Valid Height Function & Absence of Augmenting Paths

A height function $h: V \to \mathbb{Z}_{\ge 0}$ is valid with respect to residual graph $G_f$ if $h(s) = V$, $h(t) = 0$, and for every residual edge $(u, v) \in E_f$ ($c_f(u, v) > 0$):

$$h(u) \le h(v) + 1 \quad (\text{No steep downhill cliff in } G_f)$$

Crucial Consequence: Because $h(s) = V$, $h(t) = 0$, and each residual edge can drop height by at most $1$, any simple path from $s$ to $t$ in $G_f$ would require at least $V$ edges—impossible in a $V$-vertex graph! Thus no $s \leadsto t$ augmenting path ever exists in $G_f$ at any point during Push-Relabel. When all active nodes reach $e(u) = 0$, the preflow is a legal flow and immediately maximum!

The Two Primitive Operations & Complexity Bounds

  • Push($u, v$): Applicable when $u$ is active ($e(u) > 0$), $c_f(u, v) > 0$, and $h(u) = h(v) + 1$ (an admissible downhill edge). Push $\Delta = \min(e(u), c_f(u, v))$ units from $u$ to $v$. A push is saturating if $\Delta = c_f(u, v)$ and nonsaturating otherwise.
  • Relabel($u$): Applicable when $u$ is active ($e(u) > 0$) and has no admissible downhill residual edges ($h(u) \le h(v)$ for all $(u, v) \in E_f$). Set $h(u) = 1 + \min_{(u, v) \in E_f} h(v)$.

Since every active node $u$ has a residual path back to $s$ (conservation of initial source push) of length $\le V - 1$ and $h(s) = V$, $h(u)$ can never exceed $2V - 1$. This bounds total relabels to $O(V^2)$, saturating pushes to $O(V E)$, and nonsaturating pushes to $O(V^2 E)$ (or $O(V^2 \sqrt{E})$ under Highest-Label Selection + Gap Heuristic).

flowchart TD Init["Preflow Init: h[s] = V, saturate all (s, v) edges, push active nodes"] --> Pick["Pick active vertex u with e[u] > 0 (Highest h[u] first)"] Pick --> Scan["Scan residual edges (u, v) in G_f"] Scan --> Admissible{"Exists (u,v) with h[u] == h[v] + 1?"} Admissible -- "Yes" --> Push["Push min(e[u], c_f(u,v)) downhill to v"] Admissible -- "No" --> Relabel["Relabel: h[u] = 1 + min_{(u,v) in E_f} h[v]"] Push --> Check{"Any active vertex left?"} Relabel --> Check Check -- "Yes" --> Pick Check -- "No" --> Done["Return e[t] (Exact Maximum Flow)"]
Push-Relabel Vertex Heights h(u), Excess e(u) & Edmonds Blossom Contraction
O(V²√E) HLPP · Gap Heuristic · Odd Blossom

Watch Push-Relabel initialize h(s)=V, saturate source arcs to create excess e(u)>0, push flow strictly downhill along admissible arcs (h(u) == h(v)+1), relabel stuck vertices, and contract odd-length blossoms in general matching.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Initialize Preflow: Set h(S) = V = 4 and saturate all outgoing arcs from S ⇒ e(A)=8, e(B)=4. Since h(A)=0 == h(T)=0, A cannot push to T yet!
h=0 h=1 h=2 h=3 h=4 h=5 8/8 4/4 5/5 4/4 S (h=4) e=0 A (h=0) e=8 (active) B (h=0) e=4 (active) T (h=0) e=0
Vertex Heights h(u)
[S:4, A:0, B:0, T:0]
Admissible Rule
push(u,v) iff h(u) == h(v) + 1
Height Invariant
h(u) ≤ h(v) + 1 ∀residual (u,v)
[Step 1] Initialize Preflow: Set h(S) = V = 4 and saturate all outgoing arcs from S ⇒ e(A)=8, e(B)=4. Since h(A)=0 == h(T)=0, A cannot push to T yet!
Hardware Reality — Gap Heuristic & Global Relabeling

If relabeling vertex $u$ from height $k$ to $k' > k$ leaves zero vertices at height $k$ ($\text{count}[k] == 0$, a "gap" in the height profile), then no vertex with $k < h(w) < V$ can ever reach $t$ ($h(t) = 0$) because edges only step height by $1$! The Gap Heuristic immediately sets $h(w) = \max(h(w), V + 1)$ for all such vertices $w$, routing their excess straight back to $s$ and cutting practical runtime by an order of magnitude.

Push-Relabel VariantActive Node SelectionWorst-Case TimeSpace
Generic Push-RelabelArbitrary active node$O(V^2 E)$$O(V + E)$
FIFO Relabel-to-FrontFIFO queue + discharge list$O(V^3)$$O(V + E)$
Highest-Label + Gap HeuristicBucket priority queue by max $h(u)$$O(V^2 \sqrt{E})$$O(V + E)$

Production C++20 Implementation: Highest-Label Push-Relabel with Gap Heuristic

#include <vector>
#include <algorithm>

class PushRelabel {
public:
    struct Arc {
        int to;
        long long cap;
    };

    explicit PushRelabel(int n) : n(n), adj(n), excess(n, 0), h(n, 0), count(2 * n + 1, 0), buckets(2 * n + 1) {}

    void addEdge(int u, int v, long long cap) {
        if (u == v) return;
        adj[u].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({v, cap});
        adj[v].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({u, 0});
    }

    long long maxFlow(int s, int t) {
        h[s] = n;
        excess[t] = 1; // Prevent t from being added to active buckets
        count[0] = n - 1;

        for (int id : adj[s]) {
            push(s, id, arcs[id].cap);
        }

        while (maxHeight >= 0) {
            if (buckets[maxHeight].empty()) {
                --maxHeight;
                continue;
            }
            int u = buckets[maxHeight].back();
            buckets[maxHeight].pop_back();
            discharge(u, s, t);
        }
        return excess[t] - 1;
    }

private:
    int n, maxHeight = 0;
    std::vector<Arc> arcs;
    std::vector<std::vector<int>> adj;
    std::vector<long long> excess;
    std::vector<int> h, count;
    std::vector<std::vector<int>> buckets;

    void push(int u, int id, long long d) {
        if (d == 0) return;
        int v = arcs[id].to;
        if (excess[v] == 0 && h[v] < n) {
            buckets[h[v]].push_back(v);
            maxHeight = std::max(maxHeight, h[v]);
        }
        arcs[id].cap -= d;
        arcs[id ^ 1].cap += d;
        excess[u] -= d;
        excess[v] += d;
    }

    void discharge(int u, int s, int t) {
        while (excess[u] > 0) {
            int minNeighborH = 2 * n;
            for (int id : adj[u]) {
                if (arcs[id].cap == 0) continue;
                int v = arcs[id].to;
                if (h[u] == h[v] + 1) {
                    push(u, id, std::min(excess[u], arcs[id].cap));
                    if (excess[u] == 0) return;
                } else {
                    minNeighborH = std::min(minNeighborH, h[v]);
                }
            }
            // Relabel with Gap Heuristic
            int oldH = h[u];
            --count[oldH];
            if (count[oldH] == 0 && oldH < n) {
                for (int i = 0; i < n; ++i) {
                    if (i != s && i != t && h[i] > oldH && h[i] <= n) {
                        --count[h[i]];
                        h[i] = n + 1;
                    }
                }
            }
            h[u] = minNeighborH + 1;
            ++count[h[u]];
            if (h[u] >= n) return; // Excess will drain back to s in phase 2 or can be ignored for maxFlow value
        }
    }
};
Failure Mode — Reading Edge Flows Before Phase-2 Excess Drainage

When only the scalar value $|f^*| = e(t)$ is needed, nodes lifted to $h(u) \ge V$ can stop discharging because their excess can never reach $t$. However, if you need the actual per-edge flow assignment $f(u, v)$ (satisfying strict flow conservation $e(u) = 0$ everywhere), you must continue discharging nodes with $V \le h(u) < 2V$ until all remaining excess drains back into $s$.

Check yourself Question 1 of 4
Concept Check The Valid Height Function Inequality in Push-Relabel recall
Section: Preflow & Height Invariant

In Goldberg-Tarjan's Push-Relabel algorithm with $h(s) = V$ and $h(t) = 0$, what invariant must the height function $h$ satisfy for every residual edge $(u, v) \in E_f$ ($c_f(u, v) > 0$)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does always discharging an active vertex with the **maximum height $h(u)$** (Highest-Label Push-Relabel) reduce the number of nonsaturating pushes from $O(V^2 E)$ to $O(V^2 \sqrt{E})$?

Key Takeaways

Push-Relabel relaxes flow conservation into a preflow (e(u) >= 0) while strictly maintaining the cut validity invariant h(u) <= h(v) + 1 in G_f.
Because h(s) = V and h(t) = 0, no augmenting path from s to t ever exists in G_f during Push-Relabel.
Vertex heights never exceed 2V - 1, bounding total relabels to O(V^2) and saturating pushes to O(V E).
Selecting the active node with the maximum height (Highest-Label) combined with the Gap Heuristic achieves O(V^2 sqrt(E)) time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue