Cairn
algorithms · network flow · O(E² log U), U = the largest edge capacity — bounded by the capacities, not the graph's own size

↩ back to Network Flow

Capacity Scaling

Capacity Scaling is a twelfth Network Flow entry, and another path-choice rule fed into Ford-Fulkerson's general method — like Edmonds-Karp's breadth-first rule, it turns "any augmenting path is correct" into "here's a fast one," but from a different direction entirely. Edmonds-Karp's O(VE) bound comes purely from the graph's own size — how many nodes and edges — and never depends on the actual capacity values at all. Capacity Scaling's bound goes the other way: only ever use an edge whose residual capacity clears a threshold, Δ, that starts at the largest power of two no bigger than the network's biggest capacity and halves every time the current threshold runs dry. Minimum-Cost Maximum Flow's own Pitfalls section names this exact technique as "the standard fix" for a gap that page leaves open on purpose — its cheapest-path-first rule has no graph-size-only bound the way Edmonds-Karp's does, since a network with many equally-cheap paths and small bottlenecks can force augmentation counts that scale with the capacities themselves. This page builds the technique in its native, simpler setting: plain max flow, no costs anywhere, path choice driven by capacity magnitude alone.

Try it

Same four nodes and path-search order as Ford-Fulkerson's own demo — source S, sink T, and a thin bridge A→B that a fixed, deliberately bridge-preferring candidate order always tries first, whenever it's usable. What changes here is that "usable" now also means "residual capacity at least the current Δ," shown at the top of the stats line. Pick a scenario, then Step or Run through it:

Δ: — · augmenting paths: 0 · max flow so far: 0
Press Step or Run.

On the symmetric network (all four outer edges set to the same C, bridge fixed at capacity 1), the bridge-preferring order never actually gets to use the bridge: at the starting threshold (the largest power of two ≤ C), the bridge's own residual capacity of 1 never clears it, so the two direct paths S→A→T and S→B→T are all that's left — found and fully saturated in exactly 2 augmentations, for every C in the dropdown above, verified directly rather than assumed. Compare that to Ford-Fulkerson's own page: the identical bridge-preferring order, with no threshold at all, needs 2C augmentations on this exact graph — 8 at C=4, scaling linearly with C from there. Same candidate order, same graph, same final answer — the only thing standing between 2 augmentations and 2C is whether a capacity threshold gets to filter which paths are even eligible.

Bounding augmentations by capacity, not by hops

Every phase fixes one threshold Δ and only uses an edge if its current residual capacity is at least Δ — not just for the one path found, but repeatedly, until no Δ-eligible path remains at all. Only then does Δ halve and the next phase begin, continuing down to Δ = 1 (at which point every remaining edge is eligible, same as plain Ford-Fulkerson, and any last few units of flow get mopped up). Two facts make this fast, not just eventually correct:

So every phase costs at most 2E augmentations, a bound that depends only on the edge count, never on the capacities — the same flavor of guarantee Edmonds-Karp's BFS rule gives, reached by an entirely different mechanism (thresholding by size, not searching by hop count). What Edmonds-Karp buys with "always shortest," this buys with "always big enough for now" — and unlike Edmonds-Karp's rule, "big enough for now" transfers directly onto Minimum-Cost Maximum Flow's own cheapest-path-first search: among several equally-cheap augmenting paths, breaking the tie toward the biggest bottleneck first is exactly this same threshold idea, layered on top of cost instead of replacing it.

Reference implementation

The threshold check lives entirely inside findPath; everything else is Ford-Fulkerson's own augment-and-repeat loop, unchanged:

function capacityScaling(numNodes, edges, s, t) {
  const cap = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
  edges.forEach(e => { cap[e.a][e.b] += e.cap; });
  const flow = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
  let total = 0;

  const U = Math.max(...edges.map(e => e.cap));
  let delta = 1;
  while (delta * 2 <= U) delta *= 2; // largest power of two <= U

  while (delta >= 1) {
    while (true) {
      const path = findPathAtLeast(cap, flow, s, t, delta); // only edges with residual >= delta
      if (!path) break;

      let bottleneck = Infinity;
      for (let i = 0; i < path.length - 1; i++) {
        bottleneck = Math.min(bottleneck, cap[path[i]][path[i + 1]] - flow[path[i]][path[i + 1]]);
      }
      for (let i = 0; i < path.length - 1; i++) {
        const u = path[i], v = path[i + 1];
        flow[u][v] += bottleneck;
        flow[v][u] -= bottleneck;
      }
      total += bottleneck; // bottleneck itself is unthresholded -- could exceed delta
    }
    delta = Math.floor(delta / 2); // 0 after the delta=1 phase ends the outer loop
  }

  return total;
}

Pitfalls

The scaling has to actually start high — running the exact same threshold loop from Δ=1 throws away the entire guarantee, not just some of the speed. Verified directly with the Buggy — Δ starts at 1 option above, using the identical bridge-preferring candidate order as the correct run: for every C in the dropdown, it reproduces Ford-Fulkerson's own 2C augmentations exactly (8 at C=4, 16 at C=8, 32 at C=16, 64 at C=32) — because a single Δ=1 phase makes every edge eligible from the very first step, so which path gets found first goes right back to depending on search order alone, with no threshold left to rule the bridge out. The fix isn't "loop until Δ reaches 1" by itself — it's specifically starting Δ as high as the network's own capacities allow, so the biggest, most useful paths get first claim regardless of what order a particular findPath happens to try them in.

Stopping the phase loop one halving too early is a genuine wrong answer, not just a missed optimization — verified with the Buggy — stops before Δ=1 option, on the asymmetric network (S→A=4, A→T=3, S→B=3, B→T=4, bridge A→B=1, true max flow 7). The two direct paths only carry 3 units each before their tighter leg saturates, leaving exactly one more unit reachable only by routing through the bridge — S→A→B→T, the path's own bottleneck capped at 1. A loop that stops once Δ > 1 fails, rather than continuing while Δ ≥ 1, never runs the phase where that threshold would finally admit the bridge, and settles for 6 — a real answer, just the wrong one, with nothing in the run signaling that anything was skipped. The correct loop reaches all the way down to Δ = 1 and finds the bridge path there, landing on the true 7.

CBridge-preferring, no threshold (Ford-Fulkerson)Capacity Scaling, same order
482
8162
16322
32642

Complexity

Time: O(E² log U), where U is the largest edge capacity — each Δ-phase runs at most 2E augmentations (see above), each costing O(E) to search for a path in an adjacency-list residual graph, and there are O(log U) phases from the starting threshold down to 1. This demo represents the graph as a dense capacity matrix, same choice Ford-Fulkerson's and Edmonds-Karp's own demos make, so each search costs O(V²) here instead. Space: O(V²) for the capacity and flow matrices as implemented here, or O(V + E) with adjacency lists. The key contrast with Edmonds-Karp's O(VE) augmentation count: that bound can't get any worse no matter how large the capacities are, while this one can't get any worse no matter how large the graph is relative to log U — two different axes to be independent of, useful in different situations.

See Choosing a Network Flow Algorithm for how this compares against the site's other eleven Network Flow entries side by side.