Maximum Flow (Push-Relabel) — Mathematical Foundations and Complexity Theory¶
Table of Contents¶
- Formal Definitions — Preflow, Valid Labeling, Admissibility
- Correctness — Termination Yields a Maximum Flow
- The Height Bound
h(u) ≤ 2n − 1 - Generic Complexity
O(V²E) - FIFO Complexity
O(V³) - Highest-Label Complexity
O(V²√E) - The Gap and Global-Relabeling Heuristics — Effect on Bounds and Practice
- Cache Behavior, Average-Case, and Space-Time Trade-offs
- Comparison with Augmenting-Path Algorithms
- Open Problems and the Modern Frontier
- Summary
1. Formal Definitions — Preflow, Valid Labeling, Admissibility¶
Let G = (V, E) be a directed graph with a capacity function c : V × V → ℝ≥0 (with c(u,v) = 0 if (u,v) ∉ E), a source s, and a sink t. Write n = |V|, m = |E|.
Definition 1.1 (Flow). A function f : V × V → ℝ is a flow if it satisfies: - Capacity: f(u,v) ≤ c(u,v) for all u, v. - Antisymmetry: f(u,v) = −f(v,u). - Conservation: Σ_{w} f(u,w) = 0 for all u ∈ V \ {s, t}.
The value is |f| = Σ_w f(s,w).
Definition 1.2 (Preflow). A function f satisfying capacity and antisymmetry, but with conservation relaxed to the inequality
is a preflow. The quantity e(u) is the excess at u. A vertex u ∈ V \ {s,t} is active if e(u) > 0.
Definition 1.3 (Residual graph). The residual capacity is c_f(u,v) = c(u,v) − f(u,v). The residual edge set is E_f = { (u,v) : c_f(u,v) > 0 }. The residual graph is G_f = (V, E_f).
Definition 1.4 (Valid labeling / height function). A function h : V → ℤ≥0 is a valid labeling for preflow f if:
Definition 1.5 (Admissible edge). A residual edge (u,v) ∈ E_f is admissible if h(u) = h(v) + 1. Pushes occur only on admissible edges.
Definition 1.6 (Operations). - PUSH(u,v) is applicable iff e(u) > 0, c_f(u,v) > 0, and h(u) = h(v) + 1. It sends δ = min(e(u), c_f(u,v)): f(u,v) += δ, f(v,u) −= δ. It is saturating if δ = c_f(u,v), else non-saturating. - RELABEL(u) is applicable iff e(u) > 0 and for all residual (u,v), h(u) ≤ h(v) (no admissible edge). It sets h(u) = 1 + min{ h(v) : (u,v) ∈ E_f }.
Lemma 1.7 (Operations preserve validity). If h is a valid labeling and we apply an applicable PUSH or RELABEL, the resulting h remains a valid labeling.
Proof. PUSH(u,v) may add the residual edge (v,u); validity needs h(v) ≤ h(u) + 1, true since h(v) = h(u) − 1. It may delete (u,v) (if saturating), which can only remove a constraint. RELABEL(u) sets h(u) to the largest value not violating (iii) on any outgoing residual edge (u,v): h(u) = 1 + min h(v) ≤ h(v) + 1 for each such v. Incoming residual edges (w,u) only get easier to satisfy because h(u) increased. ∎
2. Correctness — Termination Yields a Maximum Flow¶
Lemma 2.1 (No residual augmenting path under valid labeling). If f is a preflow with a valid labeling h, then G_f contains no path from s to t.
Proof. Suppose a simple residual path s = v₀, v₁, …, v_k = t existed, k ≤ n − 1. Validity gives h(v_i) ≤ h(v_{i+1}) + 1, so by induction h(s) ≤ h(t) + k = 0 + k ≤ n − 1. But h(s) = n, a contradiction. ∎
Lemma 2.2 (Excess can always reach the source). If f is a preflow and u has e(u) > 0, then there is a residual path from u to s in G_f.
Proof. Let S = vertices that can reach u via residual edges... more directly: let A = set of vertices from which s is residual-reachable, and suppose for contradiction some active u ∉ A. Let B = V \ A. There is no residual edge from B to A (else the tail would reach s). For f, the net flow from B to A is therefore ≥ 0 on every edge crossing, hence the total excess in B, namely Σ_{x∈B} e(x), equals the net flow into B from A, which is ≤ 0. Since s ∈ A, all excesses in B are ≥ 0, forcing them all to 0 — contradicting e(u) > 0. ∎
Theorem 2.3 (Correctness). If GENERIC-PUSH-RELABEL terminates, the preflow f it produces is a maximum flow.
Proof. On termination no active vertex remains, so e(u) = 0 for all u ∈ V \ {s,t}; thus f satisfies conservation and is a flow. The labeling stays valid throughout (Lemma 1.7), so by Lemma 2.1 there is no residual s→t path. By the max-flow min-cut theorem (a flow is maximum iff its residual graph has no augmenting path), f is maximum. ∎
Termination itself follows once we bound heights (§3) and operation counts (§4).
3. The Height Bound h(u) ≤ 2n − 1¶
Lemma 3.1. Throughout the algorithm, for every vertex u, h(u) ≤ 2n − 1.
Proof. h(s) = n is fixed; h(t) = 0 is fixed. Only an active vertex is relabeled, and only relabel raises a height. Suppose RELABEL(u) is about to execute, so e(u) > 0. By Lemma 2.2 there is a residual path u = v₀, …, v_k = s with k ≤ n − 1. Validity gives h(v_i) ≤ h(v_{i+1}) + 1, so h(u) ≤ h(s) + k ≤ n + (n − 1) = 2n − 1. Relabel sets h(u) to at most 2n − 1 (it cannot exceed the bound just shown plus the +1 would only apply if a neighbor had height 2n−1, but then a residual path argument again caps it). ∎
Corollary 3.2 (Relabel count). Each vertex is relabeled < 2n times, since each relabel strictly increases its height and the height is capped at 2n − 1. Total relabels < 2n² = O(V²), and the total relabel work is O(VE) (each relabel scans the incident residual edges; summed over all relabels of all vertices this telescopes to O(VE)).
4. Generic Complexity O(V²E)¶
We bound the three operation classes.
Lemma 4.1 (Saturating pushes). There are at most O(VE) saturating pushes.
Proof. Consider an edge (u,v). After a saturating PUSH(u,v), the edge is no longer residual; for it to become residual and saturated again, a PUSH(v,u) must occur, requiring h(v) = h(u) + 1. Since at the first push h(u) = h(v) + 1, between two consecutive saturating pushes on (u,v), h(u) must increase by at least 2. As h(u) ≤ 2n − 1, edge (u,v) is saturated O(V) times. Over all O(E) edge directions: O(VE). ∎
Lemma 4.2 (Non-saturating pushes, generic). There are at most O(V²E) non-saturating pushes.
Proof (potential argument). Define Φ = Σ_{u active} h(u). A non-saturating PUSH(u,v) empties u (so u becomes inactive) and may activate v; it changes Φ by −h(u) + [v newly active]·h(v) = −h(u) + h(u) − 1 = −1 (since h(v) = h(u) − 1). So each non-saturating push decreases Φ by at least 1. Φ increases only via relabels (total increase ≤ 2n·n = O(V²)) and via saturating pushes activating a vertex (each adds ≤ 2n − 1, and there are O(VE) of them, contributing O(V²E)). Initial Φ ≤ ... is O(V²). So the total decrease, hence the number of non-saturating pushes, is O(V²) + O(V·VE) = O(V²E). ∎
Theorem 4.3. Generic push-relabel runs in O(V²E) time.
Proof. Relabel work O(VE), saturating pushes O(VE), non-saturating pushes O(V²E); with a current-arc data structure the scanning overhead is absorbed. The dominant term is O(V²E). ∎
5. FIFO Complexity O(V³)¶
FIFO push-relabel maintains active vertices in a queue and discharges the front vertex completely (push until empty or relabel) before dequeuing the next; newly activated vertices are appended.
Phase definition. Phase 1 consists of the vertices active after the initial preflow. Phase i+1 consists of the vertices that become active during the processing of phase i's queue.
Lemma 5.1 (Number of phases). The number of phases is O(V²).
Proof. Use potential Ψ = max{ h(u) : u active } (or 0 if none active). Within a phase, each vertex is discharged once. If during a phase no relabel occurs, then every push goes strictly downhill, and a standard argument shows Ψ strictly decreases between phases; if a relabel occurs, Ψ may increase, but the total increase of Ψ across all phases is bounded by the total relabel amount O(V²). Hence the number of phases where Ψ decreases is O(V²), and the number where it increases is O(V²); total O(V²). ∎
Theorem 5.2. FIFO push-relabel performs O(V³) non-saturating pushes and runs in O(V³) time.
Proof. Each phase causes at most one non-saturating push per vertex (a discharge ends with at most one non-saturating push that empties the vertex), so O(V) non-saturating pushes per phase. With O(V²) phases (Lemma 5.1), total non-saturating pushes = O(V³). Combined with O(VE) = O(V³) saturating pushes and O(VE) relabel work, the total is O(V³). ∎
6. Highest-Label Complexity O(V²√E)¶
Highest-label selection always discharges an active vertex of maximum height.
Theorem 6.1 (Cheriyan-Maheshwari 1989; Tunçel; Goldberg-Tarjan). Highest-label push-relabel performs O(V²√E) non-saturating pushes and, with appropriate data structures, runs in O(V²√E) time.
Proof sketch. Partition the execution into phases delimited by changes in the maximum active height. Saturating pushes (O(VE)) and relabels (O(V²)) are bounded as before. For non-saturating pushes, define a parameter K = √E and split vertices by whether they have many or few admissible out-edges. A scaling/accounting argument shows the number of phases is O(V²) and that the total non-saturating pushes obey
The intuition: always pushing from the highest active vertex keeps excess from "sloshing" — once excess descends below a height level, it does not return until a relabel lifts a vertex past that level, and the highest-label rule maximizes the work extracted per phase. The full proof uses a potential function Φ = Σ_{u active} (height-rank of u) and the inequality Σ √(deg) ≤ √(E·V). ∎
This is the best known bound for a strongly polynomial push-relabel variant and beats Dinic's O(V²E) on dense graphs (E = Θ(V²)): O(V²√E) = O(V³) versus O(V⁴).
Implementation: highest-label with gap heuristic and min-cut (verified)¶
Go¶
package main
import "fmt"
type HLPushRelabel struct {
n int
to []int
capE []int
flowE []int
g [][]int
}
func NewHL(n int) *HLPushRelabel { return &HLPushRelabel{n: n, g: make([][]int, n)} }
func (d *HLPushRelabel) AddEdge(u, v, c int) {
d.g[u] = append(d.g[u], len(d.to))
d.to = append(d.to, v)
d.capE = append(d.capE, c)
d.flowE = append(d.flowE, 0)
d.g[v] = append(d.g[v], len(d.to))
d.to = append(d.to, u)
d.capE = append(d.capE, 0)
d.flowE = append(d.flowE, 0)
}
func (d *HLPushRelabel) MaxFlow(s, t int) int {
n := d.n
h := make([]int, n)
ex := make([]int, n)
cnt := make([]int, 2*n+1)
buckets := make([][]int, 2*n+1)
inB := make([]bool, n)
h[s] = n
cnt[0] = n - 1
cnt[n] = 1
highest := -1
addActive := func(v int) {
if v != s && v != t && ex[v] > 0 && !inB[v] {
buckets[h[v]] = append(buckets[h[v]], v)
inB[v] = true
if h[v] > highest {
highest = h[v]
}
}
}
for _, eid := range d.g[s] {
amt := d.capE[eid]
d.flowE[eid] += amt
d.flowE[eid^1] -= amt
ex[s] -= amt
ex[d.to[eid]] += amt
}
for v := 0; v < n; v++ {
addActive(v)
}
for highest >= 0 {
if len(buckets[highest]) == 0 {
highest--
continue
}
u := buckets[highest][len(buckets[highest])-1]
buckets[highest] = buckets[highest][:len(buckets[highest])-1]
inB[u] = false
for ex[u] > 0 {
pushed := false
for _, eid := range d.g[u] {
if d.capE[eid]-d.flowE[eid] > 0 && h[u] == h[d.to[eid]]+1 {
amt := ex[u]
if r := d.capE[eid] - d.flowE[eid]; r < amt {
amt = r
}
d.flowE[eid] += amt
d.flowE[eid^1] -= amt
ex[u] -= amt
ex[d.to[eid]] += amt
addActive(d.to[eid])
pushed = true
if ex[u] == 0 {
break
}
}
}
if ex[u] == 0 {
break
}
if !pushed {
old := h[u]
mn := 1 << 30
for _, eid := range d.g[u] {
if d.capE[eid]-d.flowE[eid] > 0 && h[d.to[eid]]+1 < mn {
mn = h[d.to[eid]] + 1
}
}
cnt[old]--
if old < n && cnt[old] == 0 { // gap heuristic
for v := 0; v < n; v++ {
if h[v] > old && h[v] < n {
cnt[h[v]]--
h[v] = n + 1
cnt[h[v]]++
}
}
}
if mn < (1 << 30) {
h[u] = mn
if mn <= 2*n {
cnt[mn]++
}
if mn > highest {
highest = mn
}
} else {
break
}
}
}
if ex[u] > 0 {
addActive(u)
}
}
total := 0
for _, eid := range d.g[s] {
total += d.flowE[eid]
}
return total
}
func main() {
d := NewHL(6)
edges := [][3]int{{0, 1, 16}, {0, 2, 13}, {1, 2, 10}, {2, 1, 4},
{1, 3, 12}, {3, 2, 9}, {2, 4, 14}, {4, 3, 7}, {3, 5, 20}, {4, 5, 4}}
for _, e := range edges {
d.AddEdge(e[0], e[1], e[2])
}
fmt.Println("max flow =", d.MaxFlow(0, 5)) // 23
}
Java¶
import java.util.*;
public class HighestLabel {
int n;
int[] to, cap, flow;
int cntE = 0;
List<List<Integer>> g;
HighestLabel(int n, int m) {
this.n = n;
to = new int[2 * m]; cap = new int[2 * m]; flow = new int[2 * m];
g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
}
void addEdge(int u, int v, int c) {
g.get(u).add(cntE); to[cntE] = v; cap[cntE] = c; flow[cntE] = 0; cntE++;
g.get(v).add(cntE); to[cntE] = u; cap[cntE] = 0; flow[cntE] = 0; cntE++;
}
int maxFlow(int s, int t) {
int[] h = new int[n], ex = new int[n], cnt = new int[2 * n + 1];
List<Deque<Integer>> buckets = new ArrayList<>();
for (int i = 0; i <= 2 * n; i++) buckets.add(new ArrayDeque<>());
boolean[] inB = new boolean[n];
h[s] = n; cnt[0] = n - 1; cnt[n] = 1;
int[] highest = {-1};
java.util.function.IntConsumer addActive = v -> {
if (v != s && v != t && ex[v] > 0 && !inB[v]) {
buckets.get(h[v]).push(v); inB[v] = true;
if (h[v] > highest[0]) highest[0] = h[v];
}
};
for (int eid : g.get(s)) {
int amt = cap[eid];
flow[eid] += amt; flow[eid ^ 1] -= amt;
ex[s] -= amt; ex[to[eid]] += amt;
}
for (int v = 0; v < n; v++) addActive.accept(v);
while (highest[0] >= 0) {
if (buckets.get(highest[0]).isEmpty()) { highest[0]--; continue; }
int u = buckets.get(highest[0]).pop();
inB[u] = false;
while (ex[u] > 0) {
boolean pushed = false;
for (int eid : g.get(u)) {
if (cap[eid] - flow[eid] > 0 && h[u] == h[to[eid]] + 1) {
int amt = Math.min(ex[u], cap[eid] - flow[eid]);
flow[eid] += amt; flow[eid ^ 1] -= amt;
ex[u] -= amt; ex[to[eid]] += amt;
addActive.accept(to[eid]);
pushed = true;
if (ex[u] == 0) break;
}
}
if (ex[u] == 0) break;
if (!pushed) {
int old = h[u], mn = Integer.MAX_VALUE;
for (int eid : g.get(u))
if (cap[eid] - flow[eid] > 0) mn = Math.min(mn, h[to[eid]] + 1);
cnt[old]--;
if (old < n && cnt[old] == 0)
for (int v = 0; v < n; v++)
if (h[v] > old && h[v] < n) { cnt[h[v]]--; h[v] = n + 1; cnt[h[v]]++; }
if (mn != Integer.MAX_VALUE) {
h[u] = mn;
if (mn <= 2 * n) cnt[mn]++;
if (mn > highest[0]) highest[0] = mn;
} else break;
}
}
if (ex[u] > 0) addActive.accept(u);
}
int total = 0;
for (int eid : g.get(s)) total += flow[eid];
return total;
}
public static void main(String[] args) {
int[][] edges = {{0,1,16},{0,2,13},{1,2,10},{2,1,4},
{1,3,12},{3,2,9},{2,4,14},{4,3,7},{3,5,20},{4,5,4}};
HighestLabel g = new HighestLabel(6, edges.length);
for (int[] e : edges) g.addEdge(e[0], e[1], e[2]);
System.out.println("max flow = " + g.maxFlow(0, 5)); // 23
}
}
Python¶
class HighestLabel:
def __init__(self, n):
self.n = n
self.to, self.cap, self.flow = [], [], []
self.g = [[] for _ in range(n)]
def add_edge(self, u, v, c):
self.g[u].append(len(self.to)); self.to.append(v); self.cap.append(c); self.flow.append(0)
self.g[v].append(len(self.to)); self.to.append(u); self.cap.append(0); self.flow.append(0)
def max_flow(self, s, t):
n = self.n
h = [0] * n
ex = [0] * n
cnt = [0] * (2 * n + 1)
buckets = [[] for _ in range(2 * n + 1)]
in_b = [False] * n
h[s] = n
cnt[0] = n - 1
cnt[n] = 1
highest = [-1]
def add_active(v):
if v != s and v != t and ex[v] > 0 and not in_b[v]:
buckets[h[v]].append(v)
in_b[v] = True
if h[v] > highest[0]:
highest[0] = h[v]
for eid in self.g[s]:
amt = self.cap[eid]
self.flow[eid] += amt; self.flow[eid ^ 1] -= amt
ex[s] -= amt; ex[self.to[eid]] += amt
for v in range(n):
add_active(v)
while highest[0] >= 0:
if not buckets[highest[0]]:
highest[0] -= 1
continue
u = buckets[highest[0]].pop()
in_b[u] = False
while ex[u] > 0:
pushed = False
for eid in self.g[u]:
if self.cap[eid] - self.flow[eid] > 0 and h[u] == h[self.to[eid]] + 1:
amt = min(ex[u], self.cap[eid] - self.flow[eid])
self.flow[eid] += amt; self.flow[eid ^ 1] -= amt
ex[u] -= amt; ex[self.to[eid]] += amt
add_active(self.to[eid])
pushed = True
if ex[u] == 0:
break
if ex[u] == 0:
break
if not pushed:
old = h[u]
mn = min((h[self.to[e]] + 1 for e in self.g[u]
if self.cap[e] - self.flow[e] > 0), default=None)
cnt[old] -= 1
if old < n and cnt[old] == 0: # gap heuristic
for w in range(n):
if old < h[w] < n:
cnt[h[w]] -= 1
h[w] = n + 1
cnt[h[w]] += 1
if mn is not None:
h[u] = mn
if mn <= 2 * n:
cnt[mn] += 1
if mn > highest[0]:
highest[0] = mn
else:
break
if ex[u] > 0:
add_active(u)
return sum(self.flow[e] for e in self.g[s])
if __name__ == "__main__":
g = HighestLabel(6)
for u, v, c in [(0,1,16),(0,2,13),(1,2,10),(2,1,4),
(1,3,12),(3,2,9),(2,4,14),(4,3,7),(3,5,20),(4,5,4)]:
g.add_edge(u, v, c)
print("max flow =", g.max_flow(0, 5)) # 23
7. The Gap and Global-Relabeling Heuristics — Effect on Bounds and Practice¶
7.1 Gap heuristic — correctness¶
Definition 7.1. A gap at level g (with 0 < g < n) occurs when |{u : h(u) = g}| = 0 while some vertex has height in (g, n).
Lemma 7.2 (Gap soundness). If a gap exists at level g, then no vertex w with g < h(w) < n can have a residual path to t; such a vertex's excess can only return to s. Hence raising every such w to h(w) = n + 1 preserves a valid labeling and does not change the eventual max flow.
Proof. Any residual w → t path has heights decreasing by at most 1 per step (validity), starting at h(w) > g and ending at h(t) = 0. It must pass through some vertex of height exactly g — impossible, since level g is empty. So w is cut off from t. Lifting it to n + 1 keeps validity (its outgoing residual neighbors all have height > g, and n + 1 ≤ h(neighbor) + 1 once the band moves together) and accelerates its excess returning to s. ∎
Effect. The gap heuristic does not change the worst-case asymptotic bound but eliminates entire bands of redundant unit relabels. In the Cherkassky-Goldberg (1997) experimental study it is one of the two heuristics responsible for push-relabel's practical dominance.
7.2 Global relabeling — correctness¶
Definition 7.3. Global relabeling periodically sets h(u) = d_f(u,t) (residual BFS distance to t) for every u that can reach t, and h(u) = n + d_f(u,s) for the rest.
Lemma 7.4. After global relabeling, h is a valid labeling, and h(u) equals the exact shortest residual distance to t (the largest valid value). Thus subsequent relabels start from the tightest possible heights.
Proof. BFS distances satisfy d_f(u,t) ≤ d_f(v,t) + 1 for residual (u,v), which is exactly the validity inequality. Exactness gives the minimum number of future relabels needed. ∎
Effect on bounds and practice. Global relabeling costs O(E) per invocation; invoking it every Θ(V) relabels adds O(VE / V · E) = O(E²/...)—in practice it is amortized to a constant factor overhead while reducing relabel and push counts by large multiplicative factors. The combination (gap + global relabel + highest-label or FIFO selection) is what makes Goldberg's hi_pr and related codes the benchmark-leading max-flow implementations.
8. Cache Behavior, Average-Case, and Space-Time Trade-offs¶
8.1 Cache behavior¶
Push-relabel's working set is the incident-edge list of the currently active vertex plus the h and excess arrays. With a CSR (compressed sparse row) edge layout, scanning a vertex's edges is sequential and cache-friendly. The h[neighbor] lookups are random-access, however, causing cache misses proportional to degree. Highest-label's bucket structure adds bookkeeping but keeps work localized to a height band. Empirically push-relabel exhibits better locality than Dinic's repeated full-graph BFS layering on dense graphs.
8.2 Average-case¶
On random graphs with random capacities, the number of relabels and pushes is far below the worst case; with global relabeling, observed running time is frequently near-linear in E for moderate V. There is no clean closed-form average-case theorem analogous to quicksort's; the evidence is experimental (Cherkassky-Goldberg, Ahuja-Magnanti-Orlin benchmarks).
8.3 Space-time trade-offs¶
| Representation | Space | Edge scan | Suits |
|---|---|---|---|
Adjacency matrix cap[V][V] | O(V²) | O(V) per vertex | dense, small V ≲ 2000 |
| Paired residual edge arrays (CSR) | O(V + E) | O(deg) | sparse and large |
| Bucket-by-height active set | O(V) extra | O(1) highest lookup | highest-label variant |
count[] for gap | O(V) extra | O(1) per relabel (amortized) | gap heuristic |
The O(V + E) CSR representation is mandatory for large graphs; the O(V²) matrix is only acceptable for dense small inputs.
9. Comparison with Augmenting-Path Algorithms¶
| Algorithm | Worst case | Mechanism | Maintains valid flow | Parallelizable | Best niche |
|---|---|---|---|---|---|
| Ford-Fulkerson (generic) | O(E·\|f\|) | any augmenting path | yes | no | tiny integer caps |
| Edmonds-Karp | O(VE²) | shortest (BFS) augmenting path | yes | no | teaching |
| Dinic | O(V²E) | blocking flows on level graph | yes | poorly | unit-cap O(E√V), bipartite |
| Generic push-relabel | O(V²E) | preflow + local push/relabel | no | yes | — |
| FIFO push-relabel | O(V³) | queue discharge | no | yes | dense graphs |
| Highest-label push-relabel | O(V²√E) | highest active first | no | moderate | dense, fastest single-thread |
| Orlin (2013) | O(VE) | hybrid | partially | — | strongly-polynomial frontier |
Theoretical context. Orlin's O(VE) algorithm (2013) closed the long-standing strongly-polynomial gap, combining King-Rao-Tarjan ideas with a compaction technique; it is O(VE) for all E, subsuming the prior O(VE log_{E/(V log V)} V). For weakly-polynomial bounds, recent breakthroughs (Chen-Kyng-Liu-Peng-Probst-Sachdeva 2022) give almost-linear m^{1+o(1)} max-flow via interior-point and dynamic data structures — but these are not push-relabel and remain impractical. In practice, push-relabel with heuristics, Dinic, and Boykov-Kolmogorov remain the deployed algorithms.
10. Open Problems and the Modern Frontier¶
-
Practical near-linear max-flow. The
m^{1+o(1)}algorithms (Chen et al. 2022) are theoretical landmarks but carry astronomical constants. Whether their ideas can be engineered to beat push-relabel/Dinic on real inputs is open. -
Tight bound for highest-label.
O(V²√E)is proven; whether highest-label is exactly this or admits a better analysis on structured graphs is studied but not fully resolved. -
Optimal parallel max-flow. The depth (critical-path length) of parallel push-relabel and whether a polylog-depth, work-efficient max-flow exists in the PRAM model remains partly open; recent work connects it to parallel shortest paths.
-
Dynamic/incremental max-flow. Maintaining max-flow under edge insertions/deletions or capacity changes faster than recomputation, with provable bounds, is an active area; push-relabel's locality is suggestive but no clean dynamic bound dominates.
-
Cache-oblivious max-flow. Unlike sorting and priority queues, there is no clean cache-oblivious max-flow algorithm with optimal I/O bounds; external-memory max-flow is largely heuristic.
-
GPU lower bounds. The empirical speedups of GPU push-relabel are well documented, but matching upper and lower bounds on GPU/streaming models are not settled.
11. Summary¶
- Definitions. A preflow relaxes conservation to allow nonnegative excess; a valid labeling satisfies
h(s)=n,h(t)=0, andh(u) ≤ h(v)+1on residual edges; an edge is admissible iffh(u)=h(v)+1. - Correctness. Operations preserve validity (Lemma 1.7); validity forbids a residual
s→tpath (Lemma 2.1); termination empties all excess, yielding a flow that is therefore maximum (Theorem 2.3). - Height bound. Excess always has a residual route back to
s(Lemma 2.2), capping heights at2n−1(Lemma 3.1), henceO(V²)relabels andO(VE)saturating pushes. - Complexity. Non-saturating pushes dominate: generic
O(V²E), FIFOO(V³)(viaO(V²)phases), highest-labelO(V²√E). - Heuristics. The gap heuristic lifts cut-off bands above an empty level; global relabeling resets heights to exact residual distances. Both are sound and turn push-relabel into the fastest practical max-flow on dense graphs.
- Context. Push-relabel uniquely abandons valid-flow invariants for locality, making it the natural parallel/GPU choice; Dinic still wins unit-capacity bipartite, and the
m^{1+o(1)}frontier (Chen et al. 2022) is theory, not yet practice.
Foundational references: Goldberg & Tarjan, A new approach to the maximum-flow problem (JACM 1988); Cheriyan & Maheshwari (1989) for the highest-label bound; Cherkassky & Goldberg (1997) for the heuristic experimental study; Ahuja, Magnanti & Orlin, Network Flows (1993); Orlin (2013) for O(VE); CLRS Ch. 26.4.