Maximum Flow (Push-Relabel) — Junior Level¶
One-line summary: Push-Relabel is a maximum-flow algorithm that works locally: instead of finding whole source-to-sink paths (like Edmonds-Karp or Dinic), it lets water pile up at vertices (a "preflow"), gives every vertex a height, and repeatedly pushes excess water downhill along admissible edges or raises a stuck vertex until all the excess has drained into the sink.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
The maximum-flow problem asks: given a network of pipes (a directed graph) with capacities on each pipe, a source s, and a sink t, what is the largest amount of "water" you can route from s to t without any pipe exceeding its capacity?
You may already have met the augmenting-path family — Ford-Fulkerson, Edmonds-Karp, and Dinic (see the sibling topic max-flow-edmonds-karp-dinic). Those algorithms maintain a valid flow at every step (in = out at every internal vertex) and push more flow along whole s → t paths in the residual graph.
Push-Relabel (also called preflow-push, introduced by Goldberg and Tarjan in 1986–1988) takes a completely different, local view:
- It does not keep a valid flow during the run. Instead it keeps a preflow: an internal vertex is allowed to have more flow coming in than going out. The surplus is called excess.
- Every vertex gets an integer height (also called a label). The source starts at height
n(the number of vertices); everything else starts at0. - The two operations are tiny and local:
- PUSH: if a vertex
uhas excess and there is a residual edgeu → vwhose endpoint is exactly one step lower (height[u] == height[v] + 1), shove as much excess downhill as the edge allows. - RELABEL: if
uhas excess but nothing is one step below it, raiseujust high enough that some residual neighbor becomes one step below.
Water can only flow downhill, the source towers above everything, and the sink sits at the bottom. Excess that cannot reach the sink eventually gets pushed all the way back up to the source. When no internal vertex has any excess left, the preflow has become a valid maximum flow.
The payoff: push-relabel often beats Dinic on dense graphs, parallelizes far better than path-augmenting methods, and with two simple speed-ups (the gap and global-relabeling heuristics) is the fastest max-flow algorithm in practice for many inputs.
Prerequisites¶
Before this file you should be comfortable with:
- Directed graphs and adjacency lists — push-relabel walks neighbor lists constantly.
- The max-flow / min-cut idea at a basic level — read max-flow-edmonds-karp-dinic first if "augmenting path" and "residual graph" are new words.
- Residual capacity — for an edge
u → vwith capacityccarrying flowf, the residual isc − fforward andfbackward. - Queues (FIFO) — the simplest fast variant processes active vertices in a queue.
- Big-O notation —
O(V²E),O(V³). - Basic integer arithmetic and comparisons.
Optional but helpful:
- Having traced Edmonds-Karp by hand once. The contrast makes push-relabel click.
Glossary¶
| Term | Meaning |
|---|---|
| Flow network | Directed graph with a capacity c(u,v) ≥ 0 on each edge, a source s, and a sink t. |
| Flow | Assignment f(u,v) with 0 ≤ f ≤ c, antisymmetry f(u,v) = −f(v,u), and conservation (in = out) at every vertex except s, t. |
| Preflow | Like a flow but conservation is relaxed: internal vertices may have inflow ≥ outflow. |
Excess e(u) | inflow(u) − outflow(u). For a preflow, e(u) ≥ 0 for every vertex except s. |
| Active vertex | An internal vertex (≠ s, t) with e(u) > 0. These are the ones we still need to work on. |
Residual capacity c_f(u,v) | Remaining room on edge u → v: c(u,v) − f(u,v). A residual edge exists where this is > 0. |
Height / label h(u) | Integer attached to each vertex; drives where pushes are allowed. |
| Admissible edge | A residual edge u → v with h(u) == h(v) + 1. Only these may receive a push. |
| PUSH(u,v) | Move min(e(u), c_f(u,v)) units of excess from u to v along an admissible edge. |
| RELABEL(u) | Set h(u) = 1 + min{ h(v) : c_f(u,v) > 0 }, raising u so a push becomes possible. |
| Saturating push | A push that fills the edge (c_f becomes 0). |
| Non-saturating push | A push that empties u's excess but leaves residual room on the edge. |
| Min-cut | The smallest-capacity set of edges whose removal disconnects s from t. Equals the max flow (max-flow min-cut theorem). |
Core Concepts¶
1. Preflow — letting water pile up¶
In an augmenting-path algorithm, flow conservation holds at every step: nothing ever accumulates. Push-relabel deliberately breaks this. We start by saturating every edge out of the source — pumping the maximum possible amount out of s all at once. That floods the neighbors of s with excess.
Now the algorithm's whole job is to get rid of the excess: push it toward t if possible, or push it back toward s if it cannot reach t. When every internal vertex has e(u) = 0, the preflow is again a proper flow — and it is provably maximum.
2. Height (label) and the valid-labeling invariant¶
Each vertex u has a height h(u). We maintain one rule at all times — the valid-labeling invariant:
In words: you can never look down more than one level along an edge that still has room. We also fix h(s) = n and h(t) = 0 forever. The invariant guarantees that water flows downhill in single steps, which is what eventually forces all excess to the sink (or back to the source).
3. PUSH — moving excess downhill¶
PUSH(u, v): # requires e(u) > 0, c_f(u,v) > 0, h(u) == h(v)+1
d = min(e(u), c_f(u,v))
f(u,v) += d ; f(v,u) -= d # send d units; the reverse residual grows
e(u) -= d ; e(v) += d
Only admissible edges (h(u) == h(v) + 1) may be pushed. That single-step downhill rule keeps the labeling valid.
4. RELABEL — raising a stuck vertex¶
If u is active but no admissible edge leaves it (everything residual is at the same height or higher), we lift u:
RELABEL(u): # requires e(u) > 0 and no admissible edge out of u
h(u) = 1 + min{ h(v) : c_f(u,v) > 0 }
This is the smallest raise that creates a new admissible edge without breaking the invariant.
5. The generic algorithm¶
INITIALIZE-PREFLOW(s):
h[s] = n; h[everything else] = 0
for each edge (s, v): push full capacity, set e[v], e[s] accordingly
GENERIC-PUSH-RELABEL:
while some active vertex u exists:
if u has an admissible edge (u, v): PUSH(u, v)
else: RELABEL(u)
When the loop ends (no active vertices), f is a maximum flow. The order in which you pick the active vertex is what separates the variants:
- Generic (any active vertex):
O(V²E). - FIFO (process active vertices in a queue):
O(V³). - Highest-label (always pick the active vertex with the greatest height):
O(V²√E).
Big-O Summary¶
| Variant | Time | Space | Notes |
|---|---|---|---|
| Generic push-relabel | O(V²E) | O(V²) or O(V+E) | Any active-vertex order. |
| FIFO push-relabel | O(V³) | O(V+E) | Process active vertices in a queue. Simple and fast. |
| Highest-label push-relabel | O(V²√E) | O(V+E) | Always push from the highest active vertex. |
| With gap + global-relabel heuristics | same worst case, much faster in practice | O(V+E) | The version you actually ship. |
| Edmonds-Karp (for contrast) | O(VE²) | O(V+E) | Augmenting paths via BFS. |
| Dinic (for contrast) | O(V²E) | O(V+E) | Blocking flows; great on unit-capacity graphs. |
Counts: V = vertices, E = edges. Push-relabel does at most O(V²) relabels, O(VE) saturating pushes, and (depending on order) O(V²E) or fewer non-saturating pushes.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
| Heights and gravity | Picture every vertex as a platform at some altitude. Water only ever flows downhill, and only down a single step (h(u) = h(v)+1). The source is a mountaintop at altitude n; the sink is the sea at altitude 0. |
| Preflow / excess | Each platform is a bucket. We open the source's taps fully, flooding its neighbors. Now buckets are overflowing — that overflow is the excess. |
| PUSH | Tip a bucket so its overflow spills onto a lower neighboring platform. You can only spill as much as fits down the pipe (c_f) and as much as you have (e). |
| RELABEL | A bucket is overflowing but every pipe leads sideways or uphill. So you put the platform on a hydraulic lift and raise it just enough that one outgoing pipe now points downhill. |
| Pushing excess back to the source | Some water gets trapped — it can never reach the sea. As those platforms keep getting relabeled, they eventually rise above the source (height > n) and the trapped water drains back to the mountaintop. |
| Termination | When every bucket (except source and sink) is empty, the water has settled. The amount that reached the sea is the maximum flow. |
Where the analogy breaks: real water flows continuously; push-relabel moves it in discrete chunks, and a vertex can be raised many times before its water finally settles.
Pros & Cons¶
| Pros | Cons |
|---|---|
| Often the fastest max-flow algorithm in practice (with heuristics). | More moving parts than Edmonds-Karp; easy to get the bookkeeping wrong. |
| Beats Dinic on dense graphs (E ≈ V²). | The intermediate state is not a valid flow — confusing to debug. |
| Local operations — parallelizes far better than path-augmenting methods. | Worst-case O(V³) / O(V²√E) bounds need the heuristics to be realized. |
Highest-label variant O(V²√E) beats generic Dinic O(V²E) asymptotically. | Two extra heuristics (gap, global relabel) are practically mandatory for speed. |
Recovers the min-cut for free (residual reachability from s). | Needs care with capacities to avoid integer overflow on excess. |
| No need to recompute whole BFS/DFS layers between pushes. | Highest-label selection needs a bucket structure to be efficient. |
When to use: dense graphs, very large flow problems, settings where you can parallelize, or whenever Dinic is too slow.
When NOT to use: tiny graphs (just use Edmonds-Karp; the constant factors do not matter), unit-capacity bipartite matching where Hopcroft-Karp / Dinic is simpler, or when readability beats raw speed.
Step-by-Step Walkthrough¶
Tiny network, 4 vertices: s=0, a=1, b=2, t=3. Edges (capacities):
ASCII picture (max flow here is 4 — limited by total out of s = 3 + 2 = 5, but in to t = 2 + 3 = 5; the real bottleneck makes it 4 once 1→2→3 interplay is resolved; we trace it):
n = 4, so h[s] = 4, all others 0.
Init (saturate source edges):
push 0→1 full (3): f(0,1)=3, e(1)=3
push 0→2 full (2): f(0,2)=2, e(2)=2
heights: h = [4, 0, 0, 0] excess: e = [_, 3, 2, 0] (e[s] ignored)
active queue: [1, 2]
Process vertex 1 (e=3, h=0): No admissible edge yet — every residual neighbor (2 at h0, 3 at h0, and back-edge 0 at h4) is not one below. RELABEL(1): residual neighbors are 2(h0), 3(h0), 0(h4); min height = 0, so h[1] = 1.
Now 1→3 is admissible (h[1]=1 == h[3]+1). PUSH(1,3): d = min(3, cap 2) = 2. Saturates it.
1→2 is admissible (h[1]=1 == h[2]+1). PUSH(1,2): d = min(1, cap 1) = 1. Saturates it.
Vertex 1 is now empty. Enqueue 2 (it gained excess; already active).
Process vertex 2 (e=3, h=0): RELABEL needed (residual neighbors 3(h0), back-edges 0(h4), 1(h1)); min = 0 → h[2]=1. Now 2→3 admissible. PUSH(2,3): d = min(3, cap 3) = 3.
But wait — vertex 2 received 2 from source + 1 from vertex 1 = 3, and pushed all 3 to t. Vertex 1 sent 2 to t directly. Total into t: 2 + 3 = 5. But edge 2→3 has cap 3 and 1→3 has cap 2, so t can absorb at most 2 + 3 = 5. Let us double-check the source can supply it: out of s is 3 + 2 = 5. So max flow = 5 here (the 1→2 edge let vertex 1 offload its third unit through b).
No internal vertex has excess. Algorithm halts. Max flow = 5.
Re-checking the earlier "4" guess: the cross edge
1→2is exactly what lets all 5 units through, which is why tracing carefully matters. The min-cut is the source edges{0→1, 0→2}with capacity3 + 2 = 5.
The key contrast with Edmonds-Karp: we never searched for an s→t path. We only ever compared a vertex to its immediate neighbors and either pushed down or lifted up.
Code Examples¶
Example: Generic / FIFO Push-Relabel (max flow value)¶
We use an adjacency-matrix-style cap[u][v] and flow[u][v] for clarity at junior level (fine for small/dense graphs), with a FIFO queue of active vertices.
Go¶
package main
import "fmt"
type PushRelabel struct {
n int
cap [][]int
flow [][]int
adj [][]int // neighbor lists (both forward and reverse residual edges)
}
func NewPR(n int) *PushRelabel {
c := make([][]int, n)
f := make([][]int, n)
for i := range c {
c[i] = make([]int, n)
f[i] = make([]int, n)
}
return &PushRelabel{n: n, cap: c, flow: f, adj: make([][]int, n)}
}
func (g *PushRelabel) AddEdge(u, v, c int) {
if g.cap[u][v] == 0 && g.cap[v][u] == 0 {
g.adj[u] = append(g.adj[u], v)
g.adj[v] = append(g.adj[v], u)
}
g.cap[u][v] += c
}
func (g *PushRelabel) MaxFlow(s, t int) int {
n := g.n
h := make([]int, n) // heights
excess := make([]int, n) // excess at each vertex
h[s] = n
// Saturate every edge out of the source.
for _, v := range g.adj[s] {
if d := g.cap[s][v]; d > 0 {
g.flow[s][v] += d
g.flow[v][s] -= d
excess[v] += d
excess[s] -= d
}
}
// FIFO queue of active internal vertices.
queue := []int{}
inq := make([]bool, n)
for v := 0; v < n; v++ {
if v != s && v != t && excess[v] > 0 {
queue = append(queue, v)
inq[v] = true
}
}
push := func(u, v int) {
d := excess[u]
if r := g.cap[u][v] - g.flow[u][v]; r < d {
d = r
}
g.flow[u][v] += d
g.flow[v][u] -= d
excess[u] -= d
excess[v] += d
}
relabel := func(u int) {
mn := 1 << 30
for _, v := range g.adj[u] {
if g.cap[u][v]-g.flow[u][v] > 0 && h[v] < mn {
mn = h[v]
}
}
if mn < (1 << 30) {
h[u] = mn + 1
}
}
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
inq[u] = false
for excess[u] > 0 {
pushed := false
for _, v := range g.adj[u] {
if g.cap[u][v]-g.flow[u][v] > 0 && h[u] == h[v]+1 {
push(u, v)
if v != s && v != t && !inq[v] {
queue = append(queue, v)
inq[v] = true
}
pushed = true
if excess[u] == 0 {
break
}
}
}
if excess[u] == 0 {
break
}
if !pushed {
relabel(u)
}
}
}
total := 0
for _, v := range g.adj[s] {
total += g.flow[s][v]
}
return total
}
func main() {
g := NewPR(4)
for _, e := range [][3]int{{0, 1, 3}, {0, 2, 2}, {1, 2, 1}, {1, 3, 2}, {2, 3, 3}} {
g.AddEdge(e[0], e[1], e[2])
}
fmt.Println("max flow =", g.MaxFlow(0, 3)) // 5
}
Java¶
import java.util.*;
public class PushRelabel {
int n;
int[][] cap, flow;
List<List<Integer>> adj;
PushRelabel(int n) {
this.n = n;
cap = new int[n][n];
flow = new int[n][n];
adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
}
void addEdge(int u, int v, int c) {
if (cap[u][v] == 0 && cap[v][u] == 0) {
adj.get(u).add(v);
adj.get(v).add(u);
}
cap[u][v] += c;
}
int maxFlow(int s, int t) {
int[] h = new int[n];
int[] excess = new int[n];
h[s] = n;
for (int v : adj.get(s)) {
int d = cap[s][v];
if (d > 0) {
flow[s][v] += d; flow[v][s] -= d;
excess[v] += d; excess[s] -= d;
}
}
Deque<Integer> q = new ArrayDeque<>();
boolean[] inq = new boolean[n];
for (int v = 0; v < n; v++)
if (v != s && v != t && excess[v] > 0) { q.add(v); inq[v] = true; }
while (!q.isEmpty()) {
int u = q.poll();
inq[u] = false;
while (excess[u] > 0) {
boolean pushed = false;
for (int v : adj.get(u)) {
if (cap[u][v] - flow[u][v] > 0 && h[u] == h[v] + 1) {
int d = Math.min(excess[u], cap[u][v] - flow[u][v]);
flow[u][v] += d; flow[v][u] -= d;
excess[u] -= d; excess[v] += d;
if (v != s && v != t && !inq[v]) { q.add(v); inq[v] = true; }
pushed = true;
if (excess[u] == 0) break;
}
}
if (excess[u] == 0) break;
if (!pushed) {
int mn = Integer.MAX_VALUE;
for (int v : adj.get(u))
if (cap[u][v] - flow[u][v] > 0) mn = Math.min(mn, h[v]);
if (mn < Integer.MAX_VALUE) h[u] = mn + 1;
}
}
}
int total = 0;
for (int v : adj.get(s)) total += flow[s][v];
return total;
}
public static void main(String[] args) {
PushRelabel g = new PushRelabel(4);
int[][] edges = {{0, 1, 3}, {0, 2, 2}, {1, 2, 1}, {1, 3, 2}, {2, 3, 3}};
for (int[] e : edges) g.addEdge(e[0], e[1], e[2]);
System.out.println("max flow = " + g.maxFlow(0, 3)); // 5
}
}
Python¶
from collections import deque
class PushRelabel:
def __init__(self, n):
self.n = n
self.cap = [[0] * n for _ in range(n)]
self.flow = [[0] * n for _ in range(n)]
self.adj = [[] for _ in range(n)]
def add_edge(self, u, v, c):
if self.cap[u][v] == 0 and self.cap[v][u] == 0:
self.adj[u].append(v)
self.adj[v].append(u)
self.cap[u][v] += c
def max_flow(self, s, t):
n = self.n
h = [0] * n
excess = [0] * n
h[s] = n
# Saturate edges out of the source.
for v in self.adj[s]:
d = self.cap[s][v]
if d > 0:
self.flow[s][v] += d
self.flow[v][s] -= d
excess[v] += d
excess[s] -= d
q = deque(v for v in range(n) if v != s and v != t and excess[v] > 0)
inq = [False] * n
for v in q:
inq[v] = True
def push(u, v):
d = min(excess[u], self.cap[u][v] - self.flow[u][v])
self.flow[u][v] += d
self.flow[v][u] -= d
excess[u] -= d
excess[v] += d
def relabel(u):
mn = float("inf")
for v in self.adj[u]:
if self.cap[u][v] - self.flow[u][v] > 0:
mn = min(mn, h[v])
if mn < float("inf"):
h[u] = mn + 1
while q:
u = q.popleft()
inq[u] = False
while excess[u] > 0:
pushed = False
for v in self.adj[u]:
if self.cap[u][v] - self.flow[u][v] > 0 and h[u] == h[v] + 1:
push(u, v)
if v != s and v != t and not inq[v]:
q.append(v)
inq[v] = True
pushed = True
if excess[u] == 0:
break
if excess[u] == 0:
break
if not pushed:
relabel(u)
return sum(self.flow[s][v] for v in self.adj[s])
if __name__ == "__main__":
g = PushRelabel(4)
for u, v, c in [(0, 1, 3), (0, 2, 2), (1, 2, 1), (1, 3, 2), (2, 3, 3)]:
g.add_edge(u, v, c)
print("max flow =", g.max_flow(0, 3)) # 5
What it does: computes the maximum s → t flow with FIFO push-relabel. Run: go run main.go | javac PushRelabel.java && java PushRelabel | python pr.py
Coding Patterns¶
Pattern 1: "Saturate the source, then drain the excess"¶
Every push-relabel run is the same two-phase shape: (a) flood the source's neighbors, (b) loop while any active vertex remains, choosing push or relabel. Memorize this skeleton; the variants only change which active vertex you pick.
Pattern 2: Discharge¶
A common refactor groups all the work on one vertex into a single discharge(u) routine: repeatedly push along admissible edges; when none remain and u still has excess, relabel and try again. FIFO push-relabel = "repeatedly discharge the front of the queue."
def discharge(u):
while excess[u] > 0:
if no admissible edge out of u:
relabel(u)
else:
push along an admissible edge
Pattern 3: Current-arc (current-edge) pointer¶
Instead of rescanning a vertex's whole neighbor list every time, keep a pointer cur[u] into adj[u]. Advance it as edges become inadmissible; reset it to 0 on a relabel. This drops a lot of redundant scanning and is the standard way to hit the proven complexity.
Pattern 4: Min-cut recovery¶
After max flow, run a BFS/DFS from s over residual edges (c_f > 0). Vertices reachable form set S; the rest form T. The min-cut edges are the original edges from S to T. Their total capacity equals the max flow.
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
| Wrong (too small) max flow | Forgot the back-edge update flow[v][u] -= d on push. | Always update both directions; the reverse residual is what lets water flow back. |
| Infinite loop | RELABEL set h[u] no higher than before (e.g., took min of heights but forgot the +1). | h[u] = 1 + min{h[v] : c_f(u,v)>0} — the +1 is mandatory. |
IndexError / nil slice | Pushed onto a vertex never registered in adj. | Register both endpoints in adj when you first add an edge between them. |
| Excess never clears | Counting t or s as "active". | Only internal vertices (≠ s, t) are active. |
| Integer overflow on excess | Excess can transiently equal the sum of incoming capacities. | Use 64-bit accumulators when capacities are large. |
| Min-cut wrong | Used original capacities, not residual, in the reachability BFS. | Traverse only residual edges c_f(u,v) > 0. |
Performance Tips¶
- Use the FIFO variant first. It is
O(V³), simple, and fast enough for most coursework and contests. - Add the gap heuristic and global-relabel heuristic (covered in middle.md) before worrying about anything else — they typically give a 5–20× speedup.
- Switch to adjacency lists with explicit residual edges for sparse graphs; the matrix version here is
O(V²)memory and only suits small/dense inputs. - Use a current-arc pointer to avoid rescanning neighbor lists.
- For highest-label selection, keep buckets indexed by height so you can find the highest active vertex in
O(1)amortized.
Best Practices¶
- Write Edmonds-Karp first as a correctness oracle; test push-relabel against it on random small graphs.
- After termination, assert
excess[u] == 0for every internal vertex — a cheap invariant check. - Assert max-flow equals min-cut capacity in tests; it is a strong end-to-end check.
- Keep
h[s] = nandh[t] = 0fixed; never relabel the source or sink. - Document whether your graph stores parallel edges by summing capacities or by keeping them separate.
Edge Cases & Pitfalls¶
- No
s → tpath at all — max flow is 0; every unit of excess pushed out ofseventually returns tos(vertices rise above heightn). The algorithm still terminates correctly. - Self-loops — ignore them; they never carry useful flow.
- Parallel edges — either sum their capacities into
cap[u][v]or store separate residual edges; be consistent. s == t— undefined / flow is0; guard against it.- Disconnected sink — flow is
0. - Capacities of 0 — harmless but pointless; skip them when saturating.
- Huge capacities — excess can reach the sum of all out-capacities of
s; size your integer type accordingly.
Common Mistakes¶
- Treating the intermediate preflow as a valid flow — it is not; conservation is violated until the very end.
- Pushing along a non-admissible edge — only push when
h[u] == h[v] + 1. Pushing "downhill by more than one" breaks the invariant. - Relabeling without the
+1— leaves the vertex stuck and loops forever. - Forgetting to enqueue a vertex that just received excess in FIFO — its excess never gets processed.
- Re-enqueuing
sort— they must never be active. - Summing original capacities for the min-cut instead of crossing-edge capacities from the residual-reachable set.
- Relabeling the source or sink — their heights are fixed.
Cheat Sheet¶
| Concept | Rule |
|---|---|
| Init heights | h[s] = n, all others 0. |
| Init preflow | Saturate every edge out of s. |
| Active vertex | Internal vertex with e(u) > 0. |
| Admissible edge | Residual u→v with h[u] == h[v] + 1. |
| PUSH amount | min(e(u), c_f(u,v)). |
| RELABEL | h[u] = 1 + min{ h[v] : c_f(u,v) > 0 }. |
| Termination | No active vertices ⇒ preflow is max flow. |
| Min-cut | Residual-reachable-from-s set S; crossing edges S→T. |
| Generic | O(V²E) |
| FIFO | O(V³) |
| Highest-label | O(V²√E) |
Valid-labeling invariant (always true):
Visual Animation¶
See
animation.htmlfor an interactive visual animation of push-relabel.The animation demonstrates: - Per-vertex height (as vertical position) and excess (as a badge). - Admissible edges highlighted in green. - PUSH (excess flowing downhill) and RELABEL (a vertex rising) step by step. - The final maximum flow and the min-cut. - Step / Run / Reset controls.
Summary¶
Push-relabel turns max-flow on its head: instead of hunting for whole augmenting paths, it floods the source, then repeatedly does two tiny local operations — push excess downhill along admissible edges, and relabel (raise) a vertex that is stuck. A height function with the valid-labeling invariant (h(u) ≤ h(v) + 1 on residual edges) guarantees water flows downhill one step at a time, draining either to the sink or back to the source. The generic algorithm is O(V²E); FIFO selection is O(V³); highest-label is O(V²√E); and two heuristics make it the fastest max-flow method in practice. Master push and relabel, and you have mastered the whole family.
Further Reading¶
- Introduction to Algorithms (CLRS), Chapter 26 — "Maximum Flow", §26.4 "Push-relabel algorithms".
- Goldberg & Tarjan (1988), A new approach to the maximum-flow problem, JACM 35(4).
- cp-algorithms.com — "Maximum flow — Push-relabel algorithm" and "Push-relabel method improvements".
- The sibling topic max-flow-edmonds-karp-dinic — read it for the augmenting-path contrast.
- The sibling topics bipartite-matching and min-cost-max-flow — common applications.