N or P
Contents Glossary
12

Flow, capacity, and bottlenecks

Flow through a network is a polynomial problem. Image segmentation, project selection, sports elimination, ad allocation — all secretly the same problem.

The maximum-flow problem asks: given a network of pipes (or connections, or assignments, or commitments) with capacity on each, how much can flow from a source to a sink? The companion question, minimum cut, asks: what is the cheapest set of connections to sever to disconnect them? These two answers are always equal — a famous result from 1956. Both are computable in polynomial time. Many business problems that do not look like flow reduce to it: image segmentation in vision, project selection under budget, baseball elimination, advertising slot allocation, image matting, scheduling with resource constraints. When a team recognizes a flow structure, the problem moves from intractable to a library call. Rust's petgraph implements the standard flow algorithm. For larger or more nuanced flow problems, modeling as linear programming (page 19) and calling HiGHS is the production approach.

  1. 01
    Project-selection-under-budget reduces to max-flow: a source feeds each project's revenue, each project feeds its required resources, resources feed a sink with capacity equal to the budget. Run ford_fulkerson once; the max flow is the optimal portfolio value.
    use petgraph::algo::ford_fulkerson;
    use petgraph::graph::{DiGraph, NodeIndex};
    
    fn max_throughput(network: &DiGraph<&str, u32>, source: NodeIndex, sink: NodeIndex) -> u32 {
        let (flow, _flows) = ford_fulkerson(network, source, sink);
        flow
    }
  2. 02
    Three things happen on that let line. First, ford_fulkerson returns a tuple — two values bundled together in one return, no struct required. Tuples are Rust's anonymous "this thing and that thing" type, written with parentheses: the return type here is (u32, Vec<EdgeFlow>). The first slot is the maximum flow (the bottleneck capacity, the answer to "how much can move"); the second is the per-edge flow table you would need if you wanted to actually route the flow. Second, the let (flow, _flows) = … syntax is destructuring: the parentheses on the left of = mirror the tuple's shape, and each name binds to one slot. Third, the underscore prefix on _flows is the convention for "I am intentionally not using this — do not warn me about an unused variable." A bare _ discards entirely; _flows keeps the binding (mostly for documentation, so the reader knows what lives in slot two) without consuming the compiler's attention.
    // A tuple is N values bundled with parentheses.
    let point: (i32, i32)        = (3, 4);
    let pair:  (String, bool)    = (String::from("hi"), true);
    
    // Functions can return tuples — bundle several results without
    // inventing a struct for them.
    fn dimensions() -> (u32, u32) {
        (1920, 1080)
    }
    
    // Destructuring: the parentheses on the left mirror the tuple
    // shape on the right; each name binds to one position.
    let (width, height) = dimensions();
    // width = 1920, height = 1080
    
    // Underscore prefix on a name — "I'm not using this, don't warn."
    let (width, _height) = dimensions();
    //          └─────── compiler stays quiet; binding still exists
    
    // Bare underscore — discard entirely, no binding created.
    let (width, _) = dimensions();
    
    // Why _flows instead of bare _ on page 12?  Documentation.
    // The name tells the reader what would have lived in slot two.
    let (flow, _flows) = ford_fulkerson(network, source, sink);
    //   ─────  ──────
    //    │       └── per-edge flow table — held alive, unused
    //    └────────── the answer: bottleneck capacity, the max flow
    
    // To actually route the flow, drop the underscore and use it:
    let (flow, flows) = ford_fulkerson(network, source, sink);
    for edge_flow in &flows { /* ... */ }
Ford, L., Fulkerson, D. (1956) Maximal Flow Through a Network. Source →
In plain terms

The maximum-flow problem is the secret backbone of a startling fraction of business optimization problems. On its face, it is simple: water flows from a source through a network of pipes, each pipe has a capacity, how much water can reach the sink? The classical theorem — proven by Ford and Fulkerson in 1956 — is that the answer equals the minimum total capacity you would need to cut to disconnect source from sink. Flow and cut are two sides of the same coin.

What makes flow important for business planning is the long list of seemingly unrelated problems that reduce to it. Matching applicants to jobs subject to capacity constraints (each job can take so many people, each person can take so many jobs)? Max-flow. Picking which projects to fund subject to budget? Max-flow. Allocating ad slots to advertisers subject to bid and budget constraints? Max-flow. Cutting an image into foreground and background based on pixel similarity? Max-flow. Determining whether a baseball team is mathematically eliminated from playoff contention? Max-flow.

In each case, an experienced engineer recognizes the flow structure and the problem moves from a research project to a library call. The recognition is the hard part. The algorithm is decades old.

For modest-sized problems, Rust's petgraph implements the standard Edmonds-Karp algorithm directly. For larger or more nuanced problems, the right approach is to model the flow as a linear program and dispatch to an LP solver like HiGHS — modern LP solvers handle flow problems with millions of variables in seconds.

The teaching for planning: when a problem involves capacity constraints, source-to-sink shape, or matching with limits, ask whether it reduces to flow. If yes, the work is days. If no, look for other reductions before assuming the problem is novel.

Many of your hardest-looking problems are flow problems wearing different clothes.

← 11 Cheapest network that connects everything
12/39
13 Matching two sides →