Back to Graph Theory Series

Minimum-Cost Maximum Flow

October 11, 2026Wasil Zafar27 min read

Maximum flow answers “how much?” Minimum-cost maximum flow answers the operational question that follows: “how much, and through which choices at the lowest total price?”

Contents

  1. Why Cost Changes Flow
  2. The Optimization Model
  3. Residual Cancellation
  4. Successive Shortest Paths
  5. Worked Example
  6. Potentials
  7. Why It Works
  8. Implementation
  9. Applications
  10. Complexity and Limits
  11. Choosing an Algorithm

Why “Cheapest” Changes the Flow Problem

A maximum-flow algorithm treats all feasible routes as interchangeable: if two routes can each carry one unit, either is equally useful. Real systems do not work that way. One shipping lane may be slower, one worker–shift pairing may be less desirable, and one data route may consume more bandwidth. Minimum-cost maximum flow (MCMF) keeps the capacity logic of maximum flow while attaching a price to every unit that crosses an edge.

IntuitionImagine filling a venue through several entrances. Maximum flow asks how many people can enter per minute. Minimum-cost maximum flow still fills the venue as quickly as the doors allow, but prefers entrances with shorter walks, lower staffing cost, or less congestion.

How the idea evolved

1950sTransportation and flow models acquire a clean graph language: vertices are locations, edges are routes, and capacities are physical limits.
1960s+Primal–dual and shortest-path viewpoints connect network flow to linear programming and make costed flows practical.
TodayThe same model drives assignment, logistics, scheduling, ad allocation, and resource planning.

The Optimization Model

For every directed edge $(u,v)$, let $c(u,v)$ be its capacity, $w(u,v)$ its cost per unit, and $f(u,v)$ the chosen flow. A legal solution satisfies two kinds of constraints:

Capacity

No edge carries a negative amount or more than it can hold: $0 \le f(u,v) \le c(u,v)$.

Conservation

Except at source $s$ and sink $t$, every vertex sends out exactly what it receives.

The value $|f|$ is the net amount leaving the source. MCMF uses a lexicographic objective: first maximize $|f|$; among all maximum flows, choose one with minimum total cost:

$$\operatorname{cost}(f) = \sum_{(u,v)\in E} w(u,v)f(u,v)$$

Two closely related questions

Minimum-cost maximum flow: send every unit the network can possibly carry, then minimize its cost.

Minimum-cost flow of demand $D$: stop after exactly $D$ units. This fixed-demand form is often the one used in production planning.

Residual Edges Are an Undo Button

The residual network records what the algorithm may change next. If an original edge has capacity $c$, cost $w$, and currently carries flow $f$, its residual representation contains:

Residual edgeResidual capacityResidual costMeaning
Forward $u \to v$$c-f$$w$Send more flow along the original edge.
Reverse $v \to u$$f$$-w$Cancel flow that was sent earlier and refund its cost.
How a reverse residual edge appears Before sending flow, an edge from u to v has residual capacity two and cost four. After sending one unit, the forward capacity becomes one and a reverse edge of capacity one and cost negative four appears. Before pushing flow uv cap 2 · cost 4 push 1 After pushing one unit uv cap 1 · cost 4 cap 1 · cost −4 reverse edge = cancel the earlier choice
The reverse edge does not mean flow travels backward in the real system. It means the algorithm may revise its bookkeeping and reroute that unit elsewhere.

The key global insight

A locally cheapest first route need not belong to the cheapest final collection of routes. Negative-cost reverse edges let later augmentations repair earlier choices, so the algorithm is not trapped by its first decision.

Successive Shortest Augmenting Paths

The successive shortest augmenting path method turns optimization into repetition. At each round it asks: “Given everything I can still add or undo, what is the cheapest way to send the next batch from $s$ to $t$?”

One Augmentation Round
flowchart LR
    S[Residual network] --> P[Cheapest s-to-t path]
    P --> B[Path bottleneck]
    B --> U[Push and update]
    U --> S
  1. Build or maintain the residual graph.
  2. Find a minimum-cost residual path from $s$ to $t$.
  3. Let $\Delta$ be the smallest residual capacity on that path.
  4. Push $\Delta$ units, subtracting it from forward residual capacities and adding it to reverse capacities.
  5. Stop when $t$ is unreachable or when the requested demand has been delivered.

The bottleneck matters because one shortest-path search can often ship several units. That makes the number of rounds depend on the structure of the capacities, not simply on the number of edges.

A Worked Network, One Augmentation at a Time

Edge labels below use capacity · cost per unit. The highlighted route is the cheapest initial path: $s \to A \to B \to t$, with unit cost $1+0+1=2$ and bottleneck $1$.

Four-node minimum-cost flow example The source connects to A with capacity two and cost one, and to B with capacity one and cost two. A connects to B with capacity one and cost zero and to the sink with capacity one and cost three. B connects to the sink with capacity two and cost one. The path from source through A and B to the sink is highlighted. edge label = capacity · cost 2 · 1 1 · 2 1 · 0 1 · 3 2 · 1 sABt first augmenting pathavailable alternative
The first augmentation uses the teal route. Its bottleneck is the capacity-1 edge $A \to B$, so it sends one unit and creates reverse residual edges along all three segments.
RoundCheapest residual pathUnit costPushTotal flowTotal cost
1$s \to A \to B \to t$$2$$1$$1$$2$
2$s \to B \to t$$3$$1$$2$$5$
3$s \to A \to t$$4$$1$$3$$9$

After round 3, all three units of source capacity are used, so the maximum flow is $3$. The minimum cost among flows of that value is $9$. Notice the accounting pattern: each row adds push × unit cost to the running total.

Check your intuition

Suppose the cost of $A \to t$ falls from $3$ to $0$. Which path becomes cheapest first, and what is the new minimum cost for three units?

Hint: compare the three useful route costs before simulating. Answer: $s \to A \to t$ costs $1$, and the final cost becomes $6$.

Potentials Make Dijkstra Legal Again

Reverse edges can have negative cost, and ordinary Dijkstra is not correct on graphs with negative edge weights. Potentials repair that problem without changing which $s$-$t$ path is cheapest.

Maintain a potential $\pi(v)$ for each vertex and replace each residual cost with a reduced cost:

$$\widehat{w}(u,v)=w(u,v)+\pi(u)-\pi(v)$$

Every $s$-$t$ path gains the same telescoping offset, $\pi(s)-\pi(t)$. Therefore, comparing reduced path costs gives the same ordering as comparing original path costs.

1. Initialize

Use $\pi(v)=0$ when original costs are nonnegative. Otherwise compute initial shortest distances with Bellman–Ford.

2. Search

Run Dijkstra using $\widehat{w}$ on residual edges with positive capacity.

3. Update

For every reached vertex, set $\pi(v) \leftarrow \pi(v)+d(v)$.

Why does the update keep reduced costs nonnegative? Dijkstra’s distances obey $d(v) \le d(u)+\widehat{w}(u,v)$. Rearranging gives $\widehat{w}(u,v)+d(u)-d(v) \ge 0$, which is exactly the new reduced cost.

What potentials really are

A potential is a bookkeeping price attached to a vertex. It absorbs the negative reverse-edge costs into vertex offsets, leaving nonnegative edge weights for the next Dijkstra run while preserving the true path comparison.

Why the Method Works

The full proof is a primal–dual argument, but its core can be understood through three invariants:

Feasibility

Pushing no more than the bottleneck preserves capacities; augmenting along a complete $s$-$t$ path preserves conservation.

Cheapest increment

A shortest residual path is the least expensive legal way to increase the current flow value.

No cheaper repair

At optimum, the residual graph contains no negative-cost cycle that could reroute flow and lower cost without changing its value.

Reverse edges are what connect the last two ideas. Any alternative flow of the same value differs from the current flow by residual cycles. If none of those cycles has negative cost, no alternative can be cheaper. When no residual $s$-$t$ path remains, the flow is also maximum.

Implementation: Separate the Four Responsibilities

A robust solver is easier to reason about when four jobs stay distinct: edge insertion creates paired forward/reverse edges, shortest-path search records a parent edge for every vertex, augmentation updates both residual directions, and accounting updates total flow and cost.

from heapq import heappop, heappush

def cheapest_path(graph, source, sink):
    distance = [float("inf")] * len(graph)
    parent = [None] * len(graph)
    distance[source] = 0
    queue = [(0, source)]
    while queue:
        cost, vertex = heappop(queue)
        if cost != distance[vertex]:
            continue
        for target, capacity, edge_cost in graph[vertex]:
            if capacity and cost + edge_cost < distance[target]:
                distance[target] = cost + edge_cost
                parent[target] = vertex
                heappush(queue, (distance[target], target))
    return distance[sink], parent

graph = [[(1, 2, 1), (2, 2, 3)], [(3, 2, 2)], [(3, 2, 1)], []]
print(cheapest_path(graph, 0, 3)[0])  # 3
#include <iostream>
#include <queue>
#include <tuple>
#include <vector>
using namespace std;

int main() {
    vector<vector<tuple<int, int, int>>> graph(4);
    graph[0] = {{1, 2, 1}, {2, 2, 3}};
    graph[1] = {{3, 2, 2}}; graph[2] = {{3, 2, 1}};
    priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> queue;
    vector<int> distance(4, 1e9); distance[0] = 0; queue.push({0, 0});
    while (!queue.empty()) {
        auto [cost, vertex] = queue.top(); queue.pop();
        if (cost != distance[vertex]) continue;
        for (auto [target, capacity, edgeCost] : graph[vertex])
            if (capacity && cost + edgeCost < distance[target])
                distance[target] = cost + edgeCost, queue.push({distance[target], target});
    }
    cout << distance[3] << "\n"; // 3
}
import java.util.*;

public class MinCostPath {
    record Edge(int target, int capacity, int cost) {}
    public static void main(String[] args) {
        List<List<Edge>> graph = List.of(
            List.of(new Edge(1, 2, 1), new Edge(2, 2, 3)),
            List.of(new Edge(3, 2, 2)), List.of(new Edge(3, 2, 1)), List.of());
        int[] distance = {0, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE};
        PriorityQueue<int[]> queue = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
        queue.add(new int[]{0, 0});
        while (!queue.isEmpty()) {
            int[] current = queue.remove();
            if (current[0] != distance[current[1]]) continue;
            for (Edge edge : graph.get(current[1])) if (edge.capacity() > 0 && current[0] + edge.cost() < distance[edge.target()]) {
                distance[edge.target()] = current[0] + edge.cost();
                queue.add(new int[]{distance[edge.target()], edge.target()});
            }
        }
        System.out.println(distance[3]); // 3
    }
}
Edge fieldWhy it exists
toThe endpoint reached by this residual edge.
revThe index of its paired reverse edge, enabling $O(1)$ updates.
capCurrent residual capacity, not the original capacity.
costOriginal cost for a forward edge and its negation for the reverse edge.

In each round, save the exact parent edge, not merely the parent vertex; parallel edges may connect the same two vertices. Reconstruct the path from $t$ to $s$, find its minimum residual capacity, then update every chosen edge and its paired reverse edge.

Implementation checklist

  • Create the reverse edge at the same time as the forward edge.
  • Use a wide integer type for distances and total cost; the product flow × edge_cost can overflow before the final sum does.
  • Skip residual edges whose capacity is zero.
  • Update potentials only for vertices reached by the current shortest-path run.
  • Return both achieved flow and total cost; a fixed demand may be infeasible.

Where the Model Pays Off

MCMF is most natural when resources are divisible into units, constraints can be expressed as edge capacities, and preferences add linearly as per-unit costs.

Logistics

Warehouse to Store

Supply edges limit inventory, lane capacities limit transport, and costs combine freight, handling, and lateness penalties.

Assignment

Worker to Job

Unit capacities enforce one-to-one choices; the worker–job edge cost represents preference, travel, or expected effort.

Scheduling

Demand Across Time

Time-expanded layers model inventory carry-over, machine availability, and the cost of postponing work.

Allocation

Ads or Compute

Capacity limits protect budgets and resources while costs encode mismatch, latency, or opportunity cost.

Assignment as a flow network

Create edges $s \to$ worker with capacity $1$ and cost $0$, worker $\to$ job with capacity $1$ and a preference cost, and job $\to t$ with capacity $1$ and cost $0$. Maximum flow assigns as many workers as possible; minimum cost chooses the best available pairing among those maximum assignments. This construction also explains why the Hungarian algorithm is a specialized alternative for balanced one-to-one assignment.

Complexity and Limits

Let $A$ be the number of augmentations. With potentials and a binary-heap Dijkstra, the main loop costs:

$$O(AE\log V)$$

If capacities are integers, every augmentation sends at least one unit, so $A \le F$ for final flow value $F$. This gives the familiar worst-case bound $O(FE\log V)$, plus up to $O(VE)$ for a Bellman–Ford initialization when negative original costs exist. Memory usage is $O(V+E)$ after including reverse edges.

Where the simple method strains

  • Huge numeric capacities: the $F$-dependent bound is pseudo-polynomial even if each round usually pushes more than one unit.
  • Floating-point costs: equality and reduced-cost comparisons become fragile; scale to integers when the domain allows it.
  • Negative cycles: they require careful modeling and may signal an unbounded fixed-flow formulation when unlimited circulation is possible.
  • Extra business rules: logical choices, nonlinear discounts, and cross-period coupling may require linear or mixed-integer programming instead.

Choosing the Right Flow Tool

Problem shapeGood starting pointReason
Only maximize throughputDinic’s algorithmAvoids cost machinery you do not need.
Sparse network, moderate integral flow, linear costsSuccessive shortest paths + potentialsDirect, understandable, and usually practical.
Balanced one-to-one assignmentHungarian algorithmSpecialized $O(n^3)$ structure.
Very large costed-flow instancesCost scaling or network simplexBetter scaling than one augmentation at a time.
Side constraints or discrete business logicLP/MIP solverExpresses rules that do not fit ordinary flow conservation.

Mental model to keep

MCMF repeatedly buys the cheapest remaining unit of $s$-$t$ flow. Residual reverse edges let it return an earlier purchase; potentials change the price labels so Dijkstra can shop safely; the process ends when the demand is met or no route remains.