A Bit of History
Shortest paths become a batch operation
The conceptual leap is batching. Edmonds–Karp pays for a BFS before every augmentation. Dinic pays for one BFS, then extracts as much flow as possible from all shortest residual routes before the next BFS.
Start With the Residual Network
Maximum-flow algorithms never reason only about the original capacities. They work in a residual graph, whose edge $u\to v$ stores how much additional flow may still be sent. If the original capacity is $c(u,v)$ and current flow is $f(u,v)$, then:
The reverse residual edge is the algorithm’s undo button: later paths may cancel or reroute earlier flow. Every augmentation by $\Delta$ updates the paired residual capacities as $r(u,v)\mathrel{-}=\Delta$ and $r(v,u)\mathrel{+}=\Delta$.
Capacity
The fixed upper bound placed on an original directed edge.
Residual capacity
What can still move forward—or be canceled backward—right now.
Augmenting path
An $s$–$t$ path whose every edge has positive residual capacity.
One Dinic Phase
- BFS the residual graph. Assign $operatorname{level}[s]=0$ and discover every vertex reachable through positive-residual edges.
- Keep only forward layers. DFS may traverse $u\to v$ only when $r(u,v)>0$ and $operatorname{level}[v]=\operatorname{level}[u]+1$.
- Push until blocked. Repeated DFS calls send bottleneck flow. A current-edge pointer remembers which outgoing edges have already failed.
- Rebuild or stop. Once no layered $s$–$t$ path remains, run BFS again. If BFS cannot reach $t$, the residual max-flow criterion proves optimality.
The Level Graph: A Temporary DAG
BFS assigns shortest residual distances. Keeping only edges that advance exactly one level makes the usable subgraph acyclic: every DFS step strictly increases the level. Sideways edges, backward edges, and edges that skip the required next level are ignored for this phase, not deleted from the residual network.
Blocking Flow: More Than One Path
A blocking flow does not necessarily saturate every edge in the level graph. It guarantees that every layered path from $s$ to $t$ contains at least one saturated edge. Consequently, the current level graph has no remaining augmenting path.
The current-edge pointer
If DFS has proved that edge number $j$ out of vertex $u$ cannot send more flow in this level graph, later DFS calls should start after it. The pointer next[u] makes failed edges a one-time cost per phase instead of repeatedly rescanning them.
Visual Walkthrough: One Complete Phase
In this layered network, the sink-side capacities total $4+3=7$. DFS can distribute seven units across several routes. After that, both edges entering $t$ are saturated, so every layered $s$–$t$ path is blocked—even though the source still has unused outgoing capacity.
Correctness Intuition and Invariants
| Invariant | Why it stays true | Why it matters |
|---|---|---|
| Capacity | DFS pushes at most the minimum residual capacity on its path. | No edge exceeds its capacity. |
| Conservation | Every internal DFS call forwards exactly the amount it accepts. | Flow is neither created nor lost at intermediate vertices. |
| Residual reversibility | Every forward update adds the same amount to its reverse edge. | Earlier choices can be canceled or rerouted. |
| Layer progress | A blocking flow removes all $s$–$t$ paths at the current BFS distance. | The next reachable sink level is strictly larger. |
There are fewer than $V$ possible positive sink levels, so there are at most $V-1$ successful phases. When BFS finally leaves $t$ unreachable, the vertices reachable from $s$ define an $s$–$t$ cut with no positive residual edge crossing it. The current flow equals that cut’s capacity, establishing maximum flow by the max-flow/min-cut theorem.
Implementation
The compact listings below use a residual capacity matrix for readability. They correctly maintain reverse capacity and a per-vertex current-edge pointer, but scanning all $V$ possible destinations makes them best for learning or small dense graphs. For large sparse graphs, store explicit residual edge objects in adjacency lists so a phase scans actual edges rather than all vertex pairs.
Production edge representation
Represent each logical edge by two linked residual records: {to, residual, reverseIndex}. Adding capacity creates a forward record with that capacity and a reverse record with zero. Parallel and antiparallel edges then work naturally, and each update reaches its paired reverse edge in constant time.
Implementation checklist
| Detail | Correct behavior | Common failure |
|---|---|---|
| Residual test | Traverse only when residual capacity is positive. | Testing original capacity instead of remaining capacity. |
| Level test | Require level[v] == level[u] + 1. | Using merely level[v] > level[u], which changes the blocking-flow proof. |
| Reverse update | Subtract forward and add the same amount backward. | Losing the ability to undo an earlier routing choice. |
| Pointer reset | Reset current-edge pointers once per new BFS phase. | Resetting per DFS call and repeatedly scanning dead edges. |
| Numeric type | Use a type large enough for the sum of outgoing capacities. | Overflow even when each individual capacity fits. |
Complexity Analysis
On an adjacency-list residual graph, BFS costs $O(E)$. A standard blocking-flow computation costs $O(VE)$ in the general analysis, and there are fewer than $V$ successful phases:
The code above uses matrices, so its scans are expressed in terms of $V^2$ possible arcs; the classic edge-sensitive bounds assume adjacency lists. Specialized networks admit stronger results: Dinic solves bipartite matching via the usual unit-capacity reduction in $O(E\sqrt V)$, matching the familiar Hopcroft–Karp phase bound. More general unit-capacity networks have related but subtler bounds; $O(E\sqrt V)$ should not be quoted for every unit-capacity graph without the needed structure.
Practical performance
Dinic is often much faster than its general worst-case bound suggests, especially on sparse networks. Current-edge pointers, compact adjacency storage, iterative DFS when recursion is risky, and 64-bit capacities usually matter more in practice than small syntax-level optimizations.
When Should You Use Dinic?
| Algorithm | Useful when | Main tradeoff |
|---|---|---|
| Ford–Fulkerson | Teaching augmenting paths or handling tiny integer-capacity graphs. | Path choice controls performance; poor choices can be very slow. |
| Edmonds–Karp | You want the simplest deterministic polynomial implementation. | One BFS-guided augmentation at a time; $O(VE^2)$. |
| Dinic | You need a reliable general-purpose max-flow method with moderate implementation complexity. | More state and invariants than Edmonds–Karp; $O(V^2E)$ general bound. |
| Push–relabel | Dense or difficult flow instances and mature optimized implementations. | Less path-based intuition and more tuning choices. |
| Hopcroft–Karp | The problem is specifically unweighted bipartite matching. | Specialized rather than a general capacity solver. |
Real-World Applications
Matching
Convert assignments, scheduling, and bipartite matching into unit-capacity flow networks.
Routing
Measure throughput or allocate traffic when edges impose hard capacity limits.
Minimum cut
After max flow, residual reachability identifies an equal-capacity minimum cut.
The same reduction pattern appears in image segmentation, project selection, sports elimination, disjoint-path problems, and circulation models. The modeling step—choosing vertices, directions, and capacities—is often harder than running the algorithm.
Exercises
Trace the invariants
- For the visual example, list the BFS levels and verify that every highlighted edge advances exactly one level.
- Find a different sequence of DFS pushes that still produces a blocking flow of seven.
- Explain why unused capacity leaving $s$ does not imply that the current level graph is unblocked.
- Add one unit of capacity to $c\to t$. Which path can carry it, and which residual entries change?
- Challenge: replace the matrix representation with paired edge objects and confirm parallel edges are handled correctly.
Hint: after every push, check capacity, conservation, and the paired reverse residual edge separately.
Limitations
Where care is still required
- Recursion depth: a layered path can contain $O(V)$ vertices; use iterative DFS or increase stack capacity when necessary.
- Overflow: the maximum flow can exceed every single edge capacity, so totals often require 64-bit integers.
- Floating point: residual comparisons need tolerances; exact integer capacities are safer.
- Dense matrices: simple to read, but waste $O(V^2)$ memory and hide edge-sensitive performance on sparse graphs.
- Asymptotics: faster theoretical and practical alternatives exist for some graph families and extreme workloads.
Dinic remains valuable because its concepts line up cleanly with its implementation: residual pairs, BFS levels, admissible edges, DFS bottlenecks, and current-edge pruning. That makes it a strong default when you need more performance than Edmonds–Karp without moving to a substantially more complex solver.