Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lecture 11: Maximum Flow

Open In Colab

The second classical problem of this lecture is the maximum flow problem: how much can be sent through a network from one node to another when every arc has a capacity? As for the shortest path problem, and next for the traveling salesman problem, we define the problem and give an example, solve the example with an LO approach, and then solve it with a dedicated algorithm: the Ford-Fulkerson algorithm. As usual, the dedicated algorithm is often the way to go in practice. A cut then gives a simple test of whether a flow is maximal.

Learning outcomes

On completion of this notebook, you will be able to:

  • formulate the maximum flow problem as an LO model and solve it with pulp;

  • find augmenting paths, including paths that use an arc backwards, and apply the Ford-Fulkerson algorithm by hand;

  • compute the value of a cut and use a cut to show that a flow is maximal.

The Maximum Flow Problem

How much traffic can flow per hour from one part of a city to another, and which roads are the bottlenecks? Questions like this one are maximum flow problems. Other examples are whether the supply in a network can meet the demand given the capacities of the connections, and planning in which a limited resource has to be divided, such as crews in airline scheduling.

Given are a directed graph with a source node ss, a destination node dd, and a flow capacity cijc_{ij} on every arc i→ji \to j: no more than cijc_{ij} can flow from ii to jj. The goal is to find the maximum flow from ss to dd that respects

  • the capacities: the flow on an arc is at most its capacity;

  • flow conservation: what flows into a node also flows out of it, except at ss and dd.

This is similar to the transshipment problem with one warehouse and one customer, where the goal is now to deliver as much as possible and the arcs have capacities instead of costs.

As an example, we look for the maximum flow from s=As = \text{A} to d=Fd = \text{F} in Figure 1. The capacity cDF=4c_{DF} = 4 means that at most 4 can flow from D to F. A pair of nodes without an arc, such as A and D, has capacity cAD=0c_{AD} = 0.

Source
<Figure size 900x480 with 1 Axes>

Figure 1:Example network with flow capacities along the arcs.

LO Approach

Model

Decision Variables

xijx_{ij} = the flow from ii to jj, for all i,ji, j.

Objective

Maximize the flow out of the source, ∑jxsj\sum_j x_{sj}. By flow conservation, everything that leaves the source arrives at the destination, so maximizing ∑ixid\sum_i x_{id} gives the same answer. If there are arcs into the source, maximize the net outflow ∑jxsj−∑jxjs\sum_j x_{sj} - \sum_j x_{js} instead: otherwise flow could go round in a cycle back to the source and count twice.

Constraints

The flow on an arc is at most its capacity, every node other than ss and dd has flow in equal to flow out, and flows are non-negative.

Complete LO Model

max⁡∑jxsjs.t.xij≤cij,for all i,j∑ixij=∑ixji,for all j≠s,dxij≥0,for all i,j.\begin{aligned} \max \quad & \sum_{j} x_{sj} \\ \text{s.t.} \quad & x_{ij} \le c_{ij}, \quad \text{for all } i, j \\ & \sum_{i} x_{ij} = \sum_{i} x_{ji}, \quad \text{for all } j \ne s, d \\ & x_{ij} \ge 0, \quad \text{for all } i, j. \end{aligned}

Model for the Example

For Figure 1, with source A and destination F, we only need variables for the 9 arcs: a pair without an arc has capacity 0, so its flow is 0. No arc enters A, so the objective is the flow out of A.

max⁡xAB+xACs.t.xAB≤2xAC≤4xBD≤3xCB≤2xCD≤3xCE≤1xDE≤1xDF≤4xEF≤2xAB+xCB=xBD(node B)xAC=xCB+xCD+xCE(node C)xBD+xCD=xDE+xDF(node D)xCE+xDE=xEF(node E)xij≥0,for all arcs i→j.\begin{aligned} \max \quad & x_{AB} + x_{AC} \\ \text{s.t.} \quad & x_{AB} \le 2 \\ & x_{AC} \le 4 \\ & x_{BD} \le 3 \\ & x_{CB} \le 2 \\ & x_{CD} \le 3 \\ & x_{CE} \le 1 \\ & x_{DE} \le 1 \\ & x_{DF} \le 4 \\ & x_{EF} \le 2 \\ & x_{AB} + x_{CB} = x_{BD} & \text{(node B)} \\ & x_{AC} = x_{CB} + x_{CD} + x_{CE} & \text{(node C)} \\ & x_{BD} + x_{CD} = x_{DE} + x_{DF} & \text{(node D)} \\ & x_{CE} + x_{DE} = x_{EF} & \text{(node E)} \\ & x_{ij} \ge 0, \quad \text{for all arcs } i \to j. \end{aligned}

Solving the Example in pulp

As for the shortest path problem, we only create variables for arcs that exist. New is the upBound argument of pulp.LpVariable: it gives a variable an upper bound, here the capacity, so the capacity constraints need no separate lines.

status: Optimal
flows: {('A', 'B'): 2.0, ('A', 'C'): 4.0, ('B', 'D'): 3.0, ('C', 'B'): 1.0, ('C', 'D'): 2.0, ('C', 'E'): 1.0, ('D', 'E'): 1.0, ('D', 'F'): 4.0, ('E', 'F'): 2.0}
maximum flow: 6.0

The maximum flow is 6. How can we see that without a solver, and how can we be sure that no larger flow exists? That is what the Ford-Fulkerson algorithm and cuts are for.

The Ford-Fulkerson Algorithm

Augmenting Paths

Suppose we have a flow xijx_{ij} that respects the capacities and flow conservation, for example xij=0x_{ij} = 0 on all arcs. An augmenting path is a path from ss to dd that consists only of

  • forward arcs i→ji \to j whose capacity is not reached, xij<cijx_{ij} < c_{ij}, and

  • reversed arcs: arcs i→ji \to j with positive flow, xij>0x_{ij} > 0, used backwards, from jj to ii.

The key idea is that the flow from ss to dd can be increased along an augmenting path: increase the flow on every forward arc and decrease the flow on every reversed arc, by the same amount δ\delta. Every node on the path then keeps flow in equal to flow out, and dd receives δ\delta more. Decreasing the flow on a reversed arc i→ji \to j means that flow which first went from ii to jj is redirected: node ii sends it along the rest of the augmenting path instead, and node jj receives the new flow from the path in its place.

How large can δ\delta be? A forward arc has room for cij−xijc_{ij} - x_{ij} more, and the flow on a reversed arc can decrease by at most xijx_{ij}. So the largest value is

δ=min⁡{min⁡forward i→j(cij−xij), min⁡reversed i→jxij},\delta = \min\Big\{\min_{\text{forward } i \to j} (c_{ij} - x_{ij}),\ \min_{\text{reversed } i \to j} x_{ij}\Big\},

determined by the bottleneck of the path. These remaining amounts, cij−xijc_{ij} - x_{ij} forward and xijx_{ij} backward, are the available remaining capacities considering the current flow: together they form the so-called residual graph, and an augmenting path is a path from ss to dd in it.

The Algorithm

  • Start: xij=0x_{ij} = 0 for all arcs.

  • While there is an augmenting path from ss to dd:

    1. Find an augmenting path and its maximum increase in flow δ\delta.

    2. Increase the flow along the path: xij=xij+δx_{ij} = x_{ij} + \delta on its forward arcs and xij=xij−δx_{ij} = x_{ij} - \delta on its reversed arcs. This updates the available capacities.

When no augmenting path can be found, the flow is maximal. Why that is the case follows from cuts below.

Example

We apply the algorithm to Figure 1. In the figures below, the label xij/cijx_{ij}/c_{ij} of an arc gives its flow and capacity, and the augmenting path is green.

  • Iteration 1. All flows are 0, so any path from A to F is augmenting. We take A → B → D → E → F. Its bottleneck is δ=1\delta = 1, the capacity of D → E. After increasing the flow, the total flow is 1 (Figure 2).

<Figure size 900x480 with 1 Axes>

Figure 2:Iteration 1 of Ford-Fulkerson: flow 1 along A → B → D → E → F.

  • Iteration 2. A → C → B → D → F is augmenting: A → C has room 4, C → B room 2, B → D room 3−1=23 - 1 = 2 and D → F room 4. So δ=2\delta = 2, and the total flow becomes 3 (Figure 3).

<Figure size 900x480 with 1 Axes>

Figure 3:Iteration 2 of Ford-Fulkerson: flow 2 along A → C → B → D → F.

  • Iteration 3. A → C → D → F has rooms 4−2=24 - 2 = 2, 3 and 4−2=24 - 2 = 2, so δ=2\delta = 2, and the total flow becomes 5 (Figure 4).

<Figure size 900x480 with 1 Axes>

Figure 4:Iteration 3 of Ford-Fulkerson: flow 2 along A → C → D → F.

  • Iteration 4. Now A → C, B → D and D → F are full, so a path that only uses forward arcs no longer exists. An augmenting path can also send flow backwards, though: A → B → C → E → F uses the arc C → B backwards. A → B has room 2−1=12 - 1 = 1, the flow 2 on C → B can decrease by 2, C → E has room 1 and E → F room 2−1=12 - 1 = 1, so δ=1\delta = 1. The flow on C → B decreases from 2 to 1: one unit that went from C to B now goes from C to E, and B receives a unit from A in its place. The total flow becomes 6 (Figure 5).

<Figure size 900x480 with 1 Axes>

Figure 5:Iteration 4 of Ford-Fulkerson: flow 1 along A → B → C → E → F, where the arc C → B is used backwards, so its flow decreases from 2 to 1.

  • Iteration 5. Both arcs out of A are full, and A has no arcs into it that carry flow, so we cannot even leave A: there is no augmenting path. The algorithm stops with a flow of 6, the same value as pulp found.

Other choices of augmenting paths are possible and lead to the same maximum flow, possibly with different flows on the arcs.

Cuts: Checking That a Flow Is Maximal

A cut splits the nodes into a set SS that contains the source and the rest that contains the destination. The value of the cut is the total capacity of the arcs from a node in SS to a node outside SS:

c(S)=∑i∈S, j∉Scij.c(S) = \sum_{i \in S,\ j \notin S} c_{ij}.

Cutting these arcs disconnects every path from ss to dd. Every unit of flow from ss to dd has to cross from SS to the rest over one of them, so

any s-d flow≤any s-d cut value.\text{any } s\text{-}d \text{ flow} \le \text{any } s\text{-}d \text{ cut value}.

Arcs that point into SS do not count in c(S)c(S). Flow over such an arc goes back into SS, so it has to cross from SS to the rest once more: it can only lower the net amount that leaves SS, never raise it. For example, in Figure 1 the cut S={A,B}S = \{\text{A}, \text{B}\} consists of A → C and B → D and has value 4+3=74 + 3 = 7. The arc C → B also connects SS with the rest, but it points into SS, so its capacity does not count. Every cut gives an upper bound on the flow, and some bounds are better than others: the cut S={A,B,D}S = \{\text{A}, \text{B}, \text{D}\} has value cAC+cDE+cDF=4+1+4=9c_{AC} + c_{DE} + c_{DF} = 4 + 1 + 4 = 9, a weaker bound than 7.

The inequality above already gives an optimality test: if we find a flow and a cut with the same value, no flow can be larger than this flow, so it is maximal (and no cut can be smaller, so the cut is minimal). The max-flow min-cut theorem adds that such a cut always exists: the maximum flow from ss to dd equals the minimum value of an ss-dd cut. So for a maximum flow, the optimality test always works: there is always a cut with the same value.

When the Ford-Fulkerson algorithm stops, such a cut is easy to find: let SS be the set of nodes that can still be reached from ss along forward arcs with room and reversed arcs with flow. All arcs from SS to the rest are full, and all arcs into SS are empty, so the flow equals c(S)c(S). That is also why the algorithm finds a maximum flow. In the example, no node can be reached from A, so S={A}S = \{\text{A}\}, with value cAB+cAC=2+4=6c_{AB} + c_{AC} = 2 + 4 = 6 (Figure 6). This equals the flow of 6, so the flow is maximal.

<Figure size 900x480 with 1 Axes>

Figure 6:The final flow with the cut S={A}S = \{\text{A}\} (red line), with value 2+4=62 + 4 = 6, equal to the flow.

Ford-Fulkerson in Python

The function augmenting_path searches for an augmenting path from the source, node by node: from a node ii it continues over every arc i→ji \to j with room left (forward) and every arc j→ij \to i with positive flow (reversed). It returns the path as a list of steps (arc, direction), with direction +1 for a forward and -1 for a reversed arc, and also the set of nodes it could reach, which is the minimum cut SS once no path exists. The function ford_fulkerson follows the algorithm above. As for Dijkstra’s algorithm, you do not have to be able to write this code yourself, but you should be able to follow what each line does.

augmenting path A -> B -> D -> F, delta = 2
augmenting path A -> C -> D -> F, delta = 2
augmenting path A -> C -> E -> F, delta = 1
augmenting path A -> C -> D -> E -> F, delta = 1
maximum flow: 6
minimum cut S: ['A'] with value 6

The function finds different augmenting paths than we chose by hand, with the same maximum flow of 6.

<Figure size 900x480 with 1 Axes>

Figure 7:Undirected graph of Exercise 2, with capacities along the edges.

Running Time

The running time of the Ford-Fulkerson algorithm is O(f∗⋅m)O(f^* \cdot m), where f∗f^* is the value of the maximum flow and mm the number of arcs (the OO notation is explained in Complexity and Heuristics). The algorithm repeats its loop as long as there is an augmenting path, and finding one looks at every arc a few times. With integer capacities, every iteration increases the flow by at least 1, so in the worst case there are f∗f^* iterations. More refined implementations, which choose the augmenting paths more carefully, have a polynomial running time.

References

  • Koole, G. (2019). An Introduction to Business Analytics. Chapter 7, “Combinatorial Optimization” (maximum flow).