Maximum Flow

advanced40 min

The problem

A directed graph where each edge has a capacity. Send as much as possible from a source to a sink without exceeding any capacity, and with flow conserved at every other node — whatever enters must leave.

The value of this technique is less about networks and more about what reduces to it. Bipartite matching, minimum vertex cover on bipartite graphs, and several partition problems are all max-flow in disguise.

Residual capacities and reverse edges

The central mechanism: alongside every edge, store a reverse edge with capacity zero. Pushing flow forward decreases the forward capacity and increases the reverse one, which lets a later augmentation undo an earlier bad choice.

Without reverse edges, a greedy first path can block the optimum permanently.

static int[] head, nxt, to;
static long[] cap;
static int edgeCount = 0;

static void init(int n, int m) {
    head = new int[n + 1];
    Arrays.fill(head, -1);
    nxt = new int[2 * m];
    to  = new int[2 * m];
    cap = new long[2 * m];
}

static void addEdge(int u, int v, long c) {
    to[edgeCount] = v; cap[edgeCount] = c;
    nxt[edgeCount] = head[u]; head[u] = edgeCount++;

    to[edgeCount] = u; cap[edgeCount] = 0;      // reverse, zero capacity
    nxt[edgeCount] = head[v]; head[v] = edgeCount++;
}

Adding edges in pairs means edge i and edge i ^ 1 are each other’s reverse — the XOR trick makes finding the partner free. That is why edges must always be added two at a time, never one.

For an undirected edge, give the reverse edge capacity c instead of 0.

Dinic’s algorithm

Repeatedly build a level graph by BFS, then push flow along shortest augmenting paths by DFS. Faster and more predictable than pushing one path at a time.

static int[] level, iter;
static int source, sink;

static boolean bfs() {
    level = new int[head.length];
    Arrays.fill(level, -1);
    Deque<Integer> q = new ArrayDeque<>();
    level[source] = 0;
    q.add(source);

    while (!q.isEmpty()) {
        int u = q.poll();
        for (int e = head[u]; e != -1; e = nxt[e]) {
            if (cap[e] > 0 && level[to[e]] < 0) {
                level[to[e]] = level[u] + 1;
                q.add(to[e]);
            }
        }
    }
    return level[sink] >= 0;
}

static long dfs(int u, long pushed) {
    if (u == sink || pushed == 0) return pushed;

    for (; iter[u] != -1; iter[u] = nxt[iter[u]]) {
        int e = iter[u], v = to[e];
        if (cap[e] <= 0 || level[v] != level[u] + 1) continue;

        long got = dfs(v, Math.min(pushed, cap[e]));
        if (got > 0) {
            cap[e]     -= got;
            cap[e ^ 1] += got;              // return capacity to the reverse edge
            return got;
        }
    }
    return 0;
}

static long maxFlow() {
    long total = 0;
    while (bfs()) {
        iter = head.clone();                // resume each node where it left off
        long pushed;
        while ((pushed = dfs(source, Long.MAX_VALUE)) > 0) {
            total += pushed;
        }
    }
    return total;
}

The two details that make Dinic's fast

level[v] != level[u] + 1 restricts the DFS to edges that advance exactly one level. This is what forces augmenting paths to be shortest and bounds the number of BFS phases.

iter[u] is the current-arc optimization. Once an edge is known to be useless in this phase, it is never re-examined. Resetting iter from head at the start of each phase — not inside the DFS — is what keeps the total work per phase O(VE).

Dropping either turns Dinic’s into something much slower while still producing correct answers, so the bug shows up only as a timeout.

Complexity is O(V2E) in general, but O(E√V) on unit-capacity graphs, which is the bipartite matching case. In practice it handles graphs far larger than the bound suggests.

Bipartite matching

Given two groups and a list of compatible pairs, match as many as possible with each item used at most once.

Build a source connected to every left node with capacity 1, every compatible pair as capacity 1, and every right node to a sink with capacity 1. The maximum flow is the maximum matching.

// nodes: 0 = source, 1..L = left, L+1..L+R = right, L+R+1 = sink
init(L + R + 2, L + R + pairs.length);
source = 0;
sink = L + R + 1;

for (int i = 1; i <= L; i++) addEdge(source, i, 1);
for (int j = 1; j <= R; j++) addEdge(L + j, sink, 1);
for (int[] p : pairs) addEdge(p[0], L + p[1], 1);

System.out.println(maxFlow());

The capacity-1 edges from the source and to the sink are what enforce “used at most once”. Change them to k and each item may be used k times — a small edit that solves a noticeably different problem.

Max-flow min-cut

The maximum flow equals the minimum total capacity of edges whose removal disconnects source from sink. So any problem asking for a cheapest set of removals to separate two things is a max-flow problem.

To recover which edges form the cut, run the flow, then BFS from the source using only edges with remaining capacity. Reachable nodes form one side; edges crossing to unreachable nodes are the cut.

boolean[] reachable = new boolean[n + 1];
// BFS from source over edges with cap[e] > 0, marking reachable
// then any original edge u -> v with reachable[u] && !reachable[v] is in the cut

Problems that reduce to max flow

QuestionConstruction
Maximum bipartite matchingUnit capacities, source → left, right → sink
Cheapest edges to disconnect s from tMin cut = max flow
Maximum vertex-disjoint pathsSplit each node into in/out with capacity 1
Assign tasks with per-worker limitsSource → worker with capacity = limit
Maximum independent set on a bipartite graphTotal nodes minus maximum matching
Minimum path cover of a DAGNodes minus matching on the split graph

The third row is a standard trick worth knowing: to limit how often a node is used rather than an edge, split it into v_in and v_out joined by a single edge whose capacity is the limit. All incoming edges land on v_in, all outgoing leave v_out.

Is it really max flow?

Max flow is a large amount of code for a contest. Before writing it, check for a simpler route:

  • If it is bipartite matching and the graph is small, a much shorter augmenting-path matcher (Kuhn’s algorithm, roughly 15 lines) is usually enough.
  • If capacities are all 1 and you only need to know whether a perfect matching exists, Hall’s condition may answer it directly.
  • If the graph is a DAG with a natural ordering, a DP may be far simpler.

Reach for Dinic’s when capacities genuinely vary or the graph is large.

Common mistakes

  • Adding one edge instead of a pair, which breaks the e ^ 1 pairing and corrupts everything.
  • Reverse capacity c instead of 0 for a directed edge, which silently allows backward flow.
  • Sizing the edge arrays too small. You need 2 * m entries, plus room for the source and sink edges.
  • Resetting iter inside the DFS rather than once per phase, destroying the complexity.
  • Omitting the level check, which allows non-shortest paths and slows it badly.
  • int flow. With many capacity-109 edges the total needs long.
  • Recursion depth in the DFS on a long graph — the same stack caveat as any deep recursion.

Practice

  1. Given a flow network with up to 500 nodes, print the maximum flow from node 1 to node N.
  2. Given a bipartite graph, print the size of the maximum matching.
  3. Given N workers, M tasks, a compatibility list, and a per-worker task limit, print the maximum tasks assignable.
  4. Given a grid where some cells are blocked, print the maximum number of vertex-disjoint paths from the top row to the bottom row.
  5. Given a bipartite graph, print the size of the minimum vertex cover.
Hints
  1. Dinic’s directly.
  2. Unit-capacity construction. Consider Kuhn’s algorithm if the graph is small.
  3. Same as 2, but the source-to-worker capacity is the limit rather than 1.
  4. Split every cell into in/out with capacity 1 to enforce vertex-disjointness. Connect a super-source to the top row and the bottom row to a super-sink.
  5. On a bipartite graph the minimum vertex cover equals the maximum matching. This is König’s theorem — the answer is one max-flow call, not a search.

Related