CMPUT-304 · Lesson 6 of 12 · 37 min
Applications of flow: matching, circulations and project selection
Lesson 5 developed maximum flow and the max-flow min-cut theorem. The real power of flow lies in modeling: many problems that do not mention flow at all can be solved by building a suitable network, computing a maximum flow or a minimum cut, and reading off the answer. The integrality theorem is the key that makes this work for combinatorial problems, because it guarantees an optimal flow in which every edge carries a whole number, so a unit of flow can stand for a driver, a parcel, a road or a decision. This lesson works through the classic reductions: bipartite matching and vertex cover, disjoint paths, circulations with supplies and demands, project selection, and sports elimination.
Each application follows the same pattern, and it is worth making it explicit. (1) Build a network from the instance, in polynomial time. (2) Prove that every feasible solution of the original problem gives a flow of corresponding value, and that every integer flow gives a feasible solution. (3) Solve the flow problem. (4) Translate back. Step 2 has two directions, and forgetting one of them is the most common error in flow reductions: the network must neither lose good solutions nor admit fake ones.
Bipartite matching
Aspen Valley has six drivers and five vans, and each driver is licensed for only some vans. A matching in a graph is a set of edges no two of which share an endpoint; here a matching is a set of driver–van assignments with no driver or van used twice. To find a maximum matching in a bipartite graph with sides L and R, add a source s with an edge of capacity 1 to every vertex of L, a sink t with an edge of capacity 1 from every vertex of R, and direct every original edge from L to R with capacity 1. An integer flow sends at most one unit out of each driver and into each van, so the edges carrying flow from L to R form a matching of size \(|f|\). Conversely, any matching gives a flow of the same value. So the maximum flow equals the maximum matching.
from collections import deque
def max_flow(cap, s, t):
"""cap: dict {(u, v): capacity}. Returns (value, flow dict, set reachable from s at the end)."""
r, adj = {}, {}
for (u, v), c in cap.items():
r[(u, v)] = r.get((u, v), 0) + c; r.setdefault((v, u), 0)
adj.setdefault(u, set()).add(v); adj.setdefault(v, set()).add(u)
value = 0
while True:
par = {s: None}; q = deque([s])
while q and t not in par:
u = q.popleft()
for v in sorted(adj[u], key=str):
if r[(u, v)] > 0 and v not in par: par[v] = u; q.append(v)
if t not in par:
return value, {e: cap[e] - r[e] for e in cap}, set(par)
path, v = [], t
while par[v] is not None: path.append((par[v], v)); v = par[v]
b = min(r[e] for e in path)
for u, v in path: r[(u, v)] -= b; r[(v, u)] += b
value += b
# Drivers and the vans they are licensed to drive. Find the largest assignment.
licensed = {"Amir": ["V1", "V2"], "Bea": ["V1"], "Chen": ["V2", "V3", "V4"],
"Dana": ["V1", "V2"], "Eli": ["V4", "V5"], "Fern": ["V3"]}
cap = {}
for d, vans in licensed.items():
cap[("s", d)] = 1
for v in vans: cap[(d, v)] = 1
for v in {v for vs in licensed.values() for v in vs}: cap[(v, "t")] = 1
value, flow, S = max_flow(cap, "s", "t")
print("drivers assigned:", value)
for (a, b), f in sorted(flow.items()):
if f and a in licensed and b != "t": print(" ", a, "->", b)
# Konig: a minimum vertex cover = drivers NOT reachable from s, plus vans that ARE reachable.
cover = [d for d in licensed if d not in S] + sorted(v for v in S if v.startswith("V"))
print("minimum vertex cover:", cover, " size", len(cover))
assert all(d in cover or v in cover for d, vs in licensed.items() for v in vs)drivers assigned: 5 Amir -> V1 Chen -> V4 Dana -> V2 Eli -> V5 Fern -> V3 minimum vertex cover: ['Chen', 'Eli', 'Fern', 'V1', 'V2'] size 5
Since the flow value is at most \(\min(|L|, |R|) \le n\), Ford–Fulkerson makes at most n augmentations and runs in \(O(nm)\) time. Hopcroft and Karp showed in 1973 that augmenting along many vertex-disjoint shortest paths at once, which is Dinitz's algorithm specialized to this network, needs only \(O(\sqrt{n})\) phases, for \(O(m \sqrt{n})\) time. The minimum cut has a combinatorial meaning too. A vertex cover is a set of vertices touching every edge. Taking the vertices of L that are not reachable from s in the final residual graph, together with the vertices of R that are reachable, gives a vertex cover whose size equals the matching. This is König's theorem (1931): in a bipartite graph the maximum matching and the minimum vertex cover have the same size. MATH-460 proves it combinatorially, together with Hall's condition for a matching that covers all of L; here it falls out of max-flow min-cut. In general graphs minimum vertex cover is NP-hard (lesson 9), and maximum matching needs Edmonds's blossom algorithm (1965).
Disjoint paths and network reliability
How many routes from the depot to the hub share no road? If every edge gets capacity 1, an integer flow of value k decomposes into k paths that use disjoint sets of edges: follow flow out of s, which must eventually reach t, remove the path and repeat. Conversely k edge-disjoint paths give a flow of value k. By max-flow min-cut, the maximum number of edge-disjoint s–t paths equals the minimum number of edges whose removal disconnects t from s. This is Menger's theorem (1927). For an operations planner it is a reliability measure: if the depot and hub are joined by three edge-disjoint routes, any two road closures still leave a way through. Vertex-disjoint paths are handled by splitting each vertex v into an in-copy and an out-copy joined by an edge of capacity 1.
from collections import deque
def max_flow(cap, s, t):
"""cap: dict {(u, v): capacity}. Returns (value, flow dict, set reachable from s at the end)."""
r, adj = {}, {}
for (u, v), c in cap.items():
r[(u, v)] = r.get((u, v), 0) + c; r.setdefault((v, u), 0)
adj.setdefault(u, set()).add(v); adj.setdefault(v, set()).add(u)
value = 0
while True:
par = {s: None}; q = deque([s])
while q and t not in par:
u = q.popleft()
for v in sorted(adj[u], key=str):
if r[(u, v)] > 0 and v not in par: par[v] = u; q.append(v)
if t not in par:
return value, {e: cap[e] - r[e] for e in cap}, set(par)
path, v = [], t
while par[v] is not None: path.append((par[v], v)); v = par[v]
b = min(r[e] for e in path)
for u, v in path: r[(u, v)] -= b; r[(v, u)] += b
value += b
# How many edge-disjoint routes lead from the depot to the hub? (Menger: = minimum edge cut.)
roads = [("depot", "a"), ("depot", "b"), ("depot", "c"), ("a", "d"), ("b", "d"), ("b", "e"),
("c", "e"), ("d", "hub"), ("e", "hub"), ("a", "b"), ("e", "d"), ("c", "hub")]
cap = {e: 1 for e in roads}
value, flow, S = max_flow(cap, "depot", "hub")
print("edge-disjoint depot-hub routes:", value)
used = {e for e, f in flow.items() if f}
for _ in range(value): # peel off one path at a time from the flow
p, x = ["depot"], "depot"
while x != "hub":
e = next(e for e in sorted(used) if e[0] == x); used.remove(e); x = e[1]; p.append(x)
print(" ", " -> ".join(p))
print("closing these roads cuts the depot from the hub:",
[e for e in roads if e[0] in S and e[1] not in S])edge-disjoint depot-hub routes: 3
depot -> a -> d -> hub
depot -> b -> e -> hub
depot -> c -> hub
closing these roads cuts the depot from the hub: [('depot', 'a'), ('depot', 'b'), ('depot', 'c')]Circulations with supplies and demands
Two warehouses hold pallets of stock and three stores need them, and each road can carry a limited number of pallets. Can every store's demand be met? Model each warehouse w with supply \(a_w\) and each store with demand \(b_s\). Add a super source with an edge of capacity \(a_w\) to each warehouse and a super sink with an edge of capacity \(b_s\) from each store. All demands can be met exactly when the maximum flow equals the total demand. If it falls short, the minimum cut identifies the reason: a set of stores whose total demand exceeds the capacity of all roads into them. The same model handles lower bounds on edges, such as a contract requiring at least 2 pallets a day on some route: send the lower bound on the edge in advance, adjust the supplies and demands at its endpoints, and solve the remaining problem with capacity minus lower bound.
from collections import deque
def max_flow(cap, s, t):
"""cap: dict {(u, v): capacity}. Returns (value, flow dict, set reachable from s at the end)."""
r, adj = {}, {}
for (u, v), c in cap.items():
r[(u, v)] = r.get((u, v), 0) + c; r.setdefault((v, u), 0)
adj.setdefault(u, set()).add(v); adj.setdefault(v, set()).add(u)
value = 0
while True:
par = {s: None}; q = deque([s])
while q and t not in par:
u = q.popleft()
for v in sorted(adj[u], key=str):
if r[(u, v)] > 0 and v not in par: par[v] = u; q.append(v)
if t not in par:
return value, {e: cap[e] - r[e] for e in cap}, set(par)
path, v = [], t
while par[v] is not None: path.append((par[v], v)); v = par[v]
b = min(r[e] for e in path)
for u, v in path: r[(u, v)] -= b; r[(v, u)] += b
value += b
# Circulation with demands: warehouses supply pallets, stores need them, roads have capacities.
# Feasible iff the max flow from a super source to a super sink saturates all supplies.
supply = {"W1": 9, "W2": 6} # positive = supply
demand = {"S1": 5, "S2": 4, "S3": 6}
roads = {("W1", "S1"): 5, ("W1", "S2"): 3, ("W1", "X"): 4, ("W2", "X"): 6, ("X", "S2"): 3,
("X", "S3"): 6, ("W2", "S1"): 2}
def feasible(roads):
cap = dict(roads)
for w, a in supply.items(): cap[("src", w)] = a
for s, b in demand.items(): cap[(s, "snk")] = b
value, flow, _ = max_flow(cap, "src", "snk")
return value, flow
need = sum(demand.values())
value, flow = feasible(roads)
print("total demand", need, " deliverable", value, " feasible:", value == need)
roads2 = dict(roads); roads2[("X", "S2")] = 0 # road works close one road
value2, _ = feasible(roads2)
print("after closing X->S2: deliverable", value2, " feasible:", value2 == need)total demand 15 deliverable 15 feasible: True after closing X->S2: deliverable 14 feasible: False
Project selection: choosing with prerequisites
Minimum cuts are as useful as maximum flows. Aspen Valley is considering six projects. Some earn money, such as a new route or an app; others cost money, such as a new warehouse or more vans; and some projects require others. Which set of projects, closed under the prerequisite relation, gives the largest net profit? Build a network with a source s, a sink t and a vertex for each project. For a project with profit \(p > 0\), add an edge from s with capacity p; for one with cost \(c > 0\), add an edge to t with capacity c; for each requirement 'a needs b', add an edge from a to b with infinite capacity. For a cut \((S, T)\), interpret the projects in S as chosen. A finite cut cannot have an infinite edge from S to T, so S is closed under prerequisites. Its capacity is the profit of the profitable projects not chosen plus the cost of the costly projects that are chosen. Hence capacity equals (total positive profit) minus (net profit of S), and a minimum cut gives a most profitable closed set.
from collections import deque
def max_flow(cap, s, t):
"""cap: dict {(u, v): capacity}. Returns (value, flow dict, set reachable from s at the end)."""
r, adj = {}, {}
for (u, v), c in cap.items():
r[(u, v)] = r.get((u, v), 0) + c; r.setdefault((v, u), 0)
adj.setdefault(u, set()).add(v); adj.setdefault(v, set()).add(u)
value = 0
while True:
par = {s: None}; q = deque([s])
while q and t not in par:
u = q.popleft()
for v in sorted(adj[u], key=str):
if r[(u, v)] > 0 and v not in par: par[v] = u; q.append(v)
if t not in par:
return value, {e: cap[e] - r[e] for e in cap}, set(par)
path, v = [], t
while par[v] is not None: path.append((par[v], v)); v = par[v]
b = min(r[e] for e in path)
for u, v in path: r[(u, v)] -= b; r[(v, u)] += b
value += b
# Project selection as a minimum cut. Each project has a profit (positive) or cost (negative);
# some projects require others. Choose a prerequisite-closed set of maximum total profit.
profit = {"new route": 45, "night shift": 18, "app": 25, "warehouse": -50, "vans": -22, "hiring": -10}
needs = [("new route", "vans"), ("new route", "hiring"), ("night shift", "warehouse"),
("night shift", "hiring"), ("app", "warehouse")]
INF = 10 ** 9
cap = {}
for p, x in profit.items():
if x > 0: cap[("s", p)] = x
else: cap[(p, "t")] = -x
for a, b in needs: cap[(a, b)] = INF # choosing a forces choosing b
value, flow, S = max_flow(cap, "s", "t")
chosen = sorted(S - {"s"})
best = sum(x for x in profit.values() if x > 0) - value
print("chosen projects:", chosen)
print("net profit:", best, "= positive profits", sum(x for x in profit.values() if x > 0), "- min cut", value)
from itertools import combinations
names = list(profit)
brute = max(sum(profit[p] for p in A) for r in range(len(names) + 1) for A in combinations(names, r)
if all(b in A for a, b in needs if a in A))
print("brute force over all closed sets:", brute)chosen projects: ['hiring', 'new route', 'vans'] net profit: 13 = positive profits 88 - min cut 75 brute force over all closed sets: 13
The same construction, known as finding a maximum-weight closure, appears in open-pit mining (a block can be removed only after the blocks above it), in image segmentation (pixels on the source side are foreground, and cut edges penalize neighboring pixels with different labels), and in many scheduling problems.
Sports elimination
Near the end of a season, fans ask whether their team can still finish first. A team is eliminated in an obvious way if another team already has more wins than it can possibly reach. It can also be eliminated in subtle ways, when the remaining games among the other teams force one of them past it. Schwartz showed in 1966 how to decide this with flow. Assume team x wins all its remaining games, for a total B. Build a network with a vertex for each remaining game between two other teams, with capacity from s equal to the number of games left between them, and edges from each game to its two teams. Give each other team y an edge to t with capacity \(B - w_y\), the number of extra wins it can have without passing x. Team x can still finish first exactly when the maximum flow saturates every game edge out of s: every remaining game is assigned a winner without any team passing B.
from collections import deque
def max_flow(cap, s, t):
"""cap: dict {(u, v): capacity}. Returns (value, flow dict, set reachable from s at the end)."""
r, adj = {}, {}
for (u, v), c in cap.items():
r[(u, v)] = r.get((u, v), 0) + c; r.setdefault((v, u), 0)
adj.setdefault(u, set()).add(v); adj.setdefault(v, set()).add(u)
value = 0
while True:
par = {s: None}; q = deque([s])
while q and t not in par:
u = q.popleft()
for v in sorted(adj[u], key=str):
if r[(u, v)] > 0 and v not in par: par[v] = u; q.append(v)
if t not in par:
return value, {e: cap[e] - r[e] for e in cap}, set(par)
path, v = [], t
while par[v] is not None: path.append((par[v], v)); v = par[v]
b = min(r[e] for e in path)
for u, v in path: r[(u, v)] -= b; r[(v, u)] += b
value += b
# Is a team mathematically eliminated? Can team X still finish with at least as many wins
# as everyone else, if X wins all its remaining games and other results go its way?
wins = {"Otters": 83, "Hawks": 80, "Lynx": 80, "Moose": 77}
left = {("Otters", "Hawks"): 4, ("Otters", "Lynx"): 3, ("Otters", "Moose"): 3,
("Hawks", "Lynx"): 3, ("Hawks", "Moose"): 2, ("Lynx", "Moose"): 2}
def eliminated(x):
best = wins[x] + sum(g for pair, g in left.items() if x in pair)
if any(wins[y] > best for y in wins): return True
cap = {}
games = {pair: g for pair, g in left.items() if x not in pair and g}
for (a, b), g in games.items():
cap[("s", (a, b))] = g; cap[((a, b), a)] = g; cap[((a, b), b)] = g
for y in wins:
if y != x: cap[(y, "t")] = best - wins[y]
value, _, _ = max_flow(cap, "s", "t")
return value < sum(games.values()) # some game cannot be assigned without overtaking x
for team in wins:
print(f"{team:7s} wins {wins[team]} eliminated: {eliminated(team)}")Otters wins 83 eliminated: False Hawks wins 80 eliminated: False Lynx wins 80 eliminated: False Moose wins 77 eliminated: True
For the Moose in the example, the best possible total is \(77 + 7 = 84\) wins, more than any other team has now, so the elimination is not obvious. But the Otters, Hawks and Lynx still play 10 games among themselves, which must produce 10 more wins for those three teams, and they can absorb only \(1 + 4 + 4 = 9\) wins without one of them passing 84. The minimum cut contains exactly this certificate: a set of teams whose current wins plus the games left among them average more than the eliminated team can reach, here \((83 + 80 + 80 + 10)/3 > 84\).
Beyond maximum flow
Many applications need costs as well as capacities. The minimum-cost flow problem asks for a flow of given value of least total cost, and generalizes both shortest paths and maximum flow. Its special case, the assignment problem, assigns n workers to n jobs at minimum total cost and was solved by Kuhn's Hungarian method in 1955, based on work of König and Egerváry. Minimum-cost flow can be solved by repeatedly augmenting along cheapest paths in the residual graph, using Johnson's potentials from lesson 4 to keep the reduced costs nonnegative. Lesson 7 shows that all of these problems are linear programs, which explains why they share the same duality phenomena.
Worked examples
Worked problem
Hall violation
Drivers A, B and C are each licensed only for vans V1 and V2. What does the flow network show?
Show solution
- The three driver vertices have edges only to V1 and V2, and each van has a unit edge to t.
- All flow from A, B and C must pass through the two edges V1 to t and V2 to t.
- Those two edges form a cut of capacity 2, so no flow can exceed 2.
Answer: At most two of the three drivers can be assigned; the set {A, B, C} has only two neighbors, so Hall's condition fails and the cut is the certificate.
Worked problem
Splitting vertices
Transfer point m can handle at most 3 trucks per night, wherever they come from. How is this modeled?
Show solution
- Replace m by two vertices m_in and m_out.
- Send every edge into m to m_in, and every edge out of m from m_out.
- Add an edge from m_in to m_out with capacity 3.
Answer: A vertex capacity becomes an edge capacity on the split vertex.
Worked problem
Checking the project network
In the project network, why must prerequisite edges point from the project to what it needs, not the other way?
Show solution
- A cut is infinite if an infinite edge goes from S to T.
- With edge a to b, choosing a (a in S) and not b (b in T) gives an infinite cut, which is forbidden; that is exactly the rule "a needs b".
- The reverse direction would forbid choosing b without a, the wrong rule.
Answer: Edges point from each project to its prerequisites so that infinite cuts rule out infeasible plans.
Common mistakes
MistakeGive the L-to-R edges of a matching network large capacity and forget the unit edges at s and t.
InsteadThe unit edges at s and t are what stop a driver or van being used twice.
MistakeProve only that solutions give flows.
InsteadAlso prove that every integer flow gives a valid solution of the same value.
MistakeUse fractional flows from a general solver for a combinatorial problem.
InsteadUse an augmenting-path method, which returns integer flows by the integrality theorem.
MistakeModel a vertex capacity by capping the edges into the vertex separately.
InsteadSplit the vertex and put the capacity on the internal edge.
MistakeRead the project choice off the flow instead of the cut.
InsteadThe chosen projects are the source side of a minimum cut.
Summary and key terms
- A flow model is correct only if solutions map to flows and integer flows map back to solutions, preserving value.
- Bipartite matching is a unit-capacity flow; the minimum cut gives a minimum vertex cover (König).
- Edge-disjoint paths equal the minimum edge cut (Menger); split vertices for vertex-disjoint paths and vertex capacities.
- Supplies, demands and lower bounds reduce to a single maximum flow with a super source and super sink.
- Project selection and other closure problems are minimum cuts with infinite edges enforcing requirements.
- Sports elimination asks whether remaining games can be assigned without any team passing a bound.
- Matching
- A set of edges with no shared endpoints.
- Vertex cover
- A set of vertices touching every edge.
- König's theorem
- In bipartite graphs maximum matching equals minimum vertex cover.
- Menger's theorem
- Maximum number of disjoint s–t paths equals minimum s–t cut.
- Circulation with demands
- A flow problem where vertices have fixed supplies and demands.
- Closure
- A set of vertices with no edges leaving it; a prerequisite-closed set.
- Assignment problem
- Minimum-cost perfect matching in a bipartite graph.
Practice problems
Work each problem on paper first, then reveal the answer to check.
What is the running time of matching by Ford–Fulkerson on a bipartite graph with n vertices and m edges?
Show answer
O(nm), since there are at most n augmentations.
A bipartite graph has a maximum matching of size 7. How large is a minimum vertex cover?
Show answer
7, by König.
How many edge-disjoint paths join two vertices of a complete graph on 6 vertices?
Show answer
5, the degree of each vertex.
How is a lower bound of 2 on edge (u, v) removed?
Show answer
Send 2 units in advance: reduce the capacity by 2, add 2 to the demand at u and 2 to the supply at v.
In project selection, what does an edge from s with capacity 30 mean when it is cut?
Show answer
The profitable project is not chosen, losing 30.
Why is maximum matching in general graphs not handled by this network?
Show answer
The network needs two sides; general graphs need the blossom algorithm.
In the elimination network, what is the capacity of the edge from team y to t?
Show answer
B - w_y, where B is the best total of the team being tested.
What extra ingredient does minimum-cost flow add?
Show answer
A cost per unit of flow on each edge.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.