Back to Graph Theory Series

Dinic's Algorithm

September 27, 2026 Wasil Zafar 28 min read

Build the shortest-path layers once, push flow through every useful route in those layers, then rebuild only when that layered network is blocked.

Contents

  1. A Bit of History
  2. Residual Networks
  3. One Dinic Phase
  4. Level Graph
  5. Blocking Flow
  6. Visual Walkthrough
  7. Correctness Intuition
  8. Implementation
  9. Complexity Analysis
  10. When to Use It
  11. Real-World Applications
  12. Exercises
  13. Limitations

A Bit of History

Shortest paths become a batch operation

1956Ford and Fulkerson formalize augmenting paths and residual networks.
1970Yefim Dinitz publishes the level-graph and blocking-flow algorithm now commonly called Dinic’s algorithm.
1972Edmonds and Karp publish the simpler one-shortest-path-at-a-time BFS rule.

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:

$$r(u,v)=c(u,v)-f(u,v), \qquad r(v,u)=f(u,v)$$

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.

Road-lane analogy. BFS builds lanes that move exactly one step farther from the source. DFS then fills every usable lane in that temporary road plan. When all routes hit a full road, Dinic redraws the plan from the new residual network.

One Dinic Phase

  1. BFS the residual graph. Assign $operatorname{level}[s]=0$ and discover every vertex reachable through positive-residual edges.
  2. Keep only forward layers. DFS may traverse $u\to v$ only when $r(u,v)>0$ and $operatorname{level}[v]=\operatorname{level}[u]+1$.
  3. Push until blocked. Repeated DFS calls send bottleneck flow. A current-edge pointer remembers which outgoing edges have already failed.
  4. 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.

Residual graph transformed into a Dinic level graph The left panel shows a residual network. The right panel places vertices into BFS levels and highlights only positive-residual edges that advance one level. Residual network Labels show remaining capacity. 543343side edge sabcdt BFS LEVEL 0LEVEL 1LEVEL 2LEVEL 3 sabcdt Only level[v] = level[u] + 1 remains usable.
BFS creates structure. The side edge can still exist in the residual graph, yet DFS ignores it because it does not advance exactly one level. The retained graph is a shortest-path DAG for this phase.

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.

A blocking flow of seven units in one Dinic phase A four-layer network sends seven total units along several paths. Both edges into the sink are saturated, blocking every path in the level graph. Blocking flow after repeated DFS pushes Edge labels are flow / capacity. 5 / 52 / 43 / 32 / 21 / 21 / 24 / 43 / 3 sabcdt PHASE RESULT 7 units pushed c → t saturated d → t saturated ALL PATHS BLOCKED “Blocked” means every layered s–t path meets a saturated edge—not that every edge is full.
One BFS, several pushes. The blocking flow sends $3$ through $a\to c$, $2$ through $a\to d$, and one unit from $b$ into each sink-side branch. The source retains spare capacity, but no path reaches $t$ in this level graph.
Core intuition: BFS chooses the current shortest distance scale; DFS exhausts that entire scale before another BFS is allowed.

Correctness Intuition and Invariants

InvariantWhy it stays trueWhy it matters
CapacityDFS pushes at most the minimum residual capacity on its path.No edge exceeds its capacity.
ConservationEvery internal DFS call forwards exactly the amount it accepts.Flow is neither created nor lost at intermediate vertices.
Residual reversibilityEvery forward update adds the same amount to its reverse edge.Earlier choices can be canceled or rerouted.
Layer progressA 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.

from collections import deque

def dinic_max_flow(n, capacity, source, sink):
    """
    n: number of vertices. capacity: n x n matrix of edge capacities (0 if no edge).
    Returns the maximum flow from source to sink.
    """
    if source == sink:
        raise ValueError("source and sink must differ")
    capacity = [row[:] for row in capacity]  # preserve the caller's matrix
    flow_limit = sum(capacity[source])

    def bfs_level_graph():
        level = [-1] * n
        level[source] = 0
        q = deque([source])
        while q:
            u = q.popleft()
            for v in range(n):
                if capacity[u][v] > 0 and level[v] == -1:
                    level[v] = level[u] + 1
                    q.append(v)
        return level if level[sink] != -1 else None

    def dfs_blocking_flow(u, pushed, level, it):
        if u == sink or pushed == 0:
            return pushed
        while it[u] < n:
            v = it[u]
            if capacity[u][v] > 0 and level[v] == level[u] + 1:
                d = dfs_blocking_flow(v, min(pushed, capacity[u][v]), level, it)
                if d > 0:
                    capacity[u][v] -= d
                    capacity[v][u] += d
                    return d
            it[u] += 1
        return 0

    max_flow = 0
    while True:
        level = bfs_level_graph()
        if level is None:
            break
        it = [0] * n
        while True:
            pushed = dfs_blocking_flow(source, flow_limit, level, it)
            if pushed == 0:
                break
            max_flow += pushed

    return max_flow

capacity = [
    [0, 10, 10, 0, 0, 0],
    [0, 0, 2, 4, 8, 0],
    [0, 0, 0, 0, 9, 0],
    [0, 0, 0, 0, 0, 10],
    [0, 0, 0, 6, 0, 10],
    [0, 0, 0, 0, 0, 0],
]
print(dinic_max_flow(6, capacity, 0, 5))  # 19
#include <vector>
#include <queue>
#include <limits>
#include <iostream>
#include <algorithm>
#include <stdexcept>
using namespace std;

int n;
vector<vector<int>> capacity_;
vector<int> level, it;

bool bfsLevelGraph(int source, int sink) {
    level.assign(n, -1);
    level[source] = 0;
    queue<int> q;
    q.push(source);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v = 0; v < n; v++) {
            if (capacity_[u][v] > 0 && level[v] == -1) {
                level[v] = level[u] + 1;
                q.push(v);
            }
        }
    }
    return level[sink] != -1;
}

int dfsBlockingFlow(int u, int sink, int pushed) {
    if (u == sink || pushed == 0) return pushed;
    for (; it[u] < n; it[u]++) {
        int v = it[u];
        if (capacity_[u][v] > 0 && level[v] == level[u] + 1) {
            int d = dfsBlockingFlow(v, sink, min(pushed, capacity_[u][v]));
            if (d > 0) {
                capacity_[u][v] -= d;
                capacity_[v][u] += d;
                return d;
            }
        }
    }
    return 0;
}

int dinicMaxFlow(int nn, vector<vector<int>> cap, int source, int sink) {
    if (source == sink) throw invalid_argument("source and sink must differ");
    n = nn; capacity_ = cap;
    int maxFlow = 0;
    while (bfsLevelGraph(source, sink)) {
        it.assign(n, 0);
        int pushed;
        while ((pushed = dfsBlockingFlow(source, sink, numeric_limits<int>::max())) > 0) {
            maxFlow += pushed;
        }
    }
    return maxFlow;
}

int main() {
    vector<vector<int>> capacity = {
        {0,10,10,0,0,0}, {0,0,2,4,8,0}, {0,0,0,0,9,0},
        {0,0,0,0,0,10}, {0,0,0,6,0,10}, {0,0,0,0,0,0}
    };
    cout << dinicMaxFlow(6, capacity, 0, 5) << endl;  // 19
    return 0;
}
import java.util.*;

class Dinic {
    static int n;
    static int[][] capacity;
    static int[] level, it;

    static boolean bfsLevelGraph(int source, int sink) {
        level = new int[n];
        Arrays.fill(level, -1);
        level[source] = 0;
        Deque<Integer> q = new ArrayDeque<>();
        q.add(source);
        while (!q.isEmpty()) {
            int u = q.poll();
            for (int v = 0; v < n; v++) {
                if (capacity[u][v] > 0 && level[v] == -1) {
                    level[v] = level[u] + 1;
                    q.add(v);
                }
            }
        }
        return level[sink] != -1;
    }

    static int dfsBlockingFlow(int u, int sink, int pushed) {
        if (u == sink || pushed == 0) return pushed;
        for (; it[u] < n; it[u]++) {
            int v = it[u];
            if (capacity[u][v] > 0 && level[v] == level[u] + 1) {
                int d = dfsBlockingFlow(v, sink, Math.min(pushed, capacity[u][v]));
                if (d > 0) {
                    capacity[u][v] -= d;
                    capacity[v][u] += d;
                    return d;
                }
            }
        }
        return 0;
    }

    static int maxFlow(int nn, int[][] cap, int source, int sink) {
        if (source == sink) throw new IllegalArgumentException("source and sink must differ");
        n = nn;
        capacity = new int[n][];
        for (int i = 0; i < n; i++) capacity[i] = cap[i].clone();
        int maxFlow = 0;
        while (bfsLevelGraph(source, sink)) {
            it = new int[n];
            int pushed;
            while ((pushed = dfsBlockingFlow(source, sink, Integer.MAX_VALUE)) > 0) {
                maxFlow += pushed;
            }
        }
        return maxFlow;
    }

    public static void main(String[] args) {
        int[][] capacity = {
            {0,10,10,0,0,0}, {0,0,2,4,8,0}, {0,0,0,0,9,0},
            {0,0,0,0,0,10}, {0,0,0,6,0,10}, {0,0,0,0,0,0}
        };
        System.out.println(maxFlow(6, capacity, 0, 5));  // 19
    }
}

Implementation checklist

DetailCorrect behaviorCommon failure
Residual testTraverse only when residual capacity is positive.Testing original capacity instead of remaining capacity.
Level testRequire level[v] == level[u] + 1.Using merely level[v] > level[u], which changes the blocking-flow proof.
Reverse updateSubtract forward and add the same amount backward.Losing the ability to undo an earlier routing choice.
Pointer resetReset current-edge pointers once per new BFS phase.Resetting per DFS call and repeatedly scanning dead edges.
Numeric typeUse 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:

$$T(V,E)=O(V^2E), \qquad S(V,E)=O(V+E)$$

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?

AlgorithmUseful whenMain tradeoff
Ford–FulkersonTeaching augmenting paths or handling tiny integer-capacity graphs.Path choice controls performance; poor choices can be very slow.
Edmonds–KarpYou want the simplest deterministic polynomial implementation.One BFS-guided augmentation at a time; $O(VE^2)$.
DinicYou 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–relabelDense or difficult flow instances and mature optimized implementations.Less path-based intuition and more tuning choices.
Hopcroft–KarpThe 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

  1. For the visual example, list the BFS levels and verify that every highlighted edge advances exactly one level.
  2. Find a different sequence of DFS pushes that still produces a blocking flow of seven.
  3. Explain why unused capacity leaving $s$ does not imply that the current level graph is unblocked.
  4. Add one unit of capacity to $c\to t$. Which path can carry it, and which residual entries change?
  5. 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.