Learn minimum cost flow optimization for slotting problems, including network flow theory, mathematical formulation, and practical implementation with OR-Tools. Master resource allocation across time slots, capacity constraints, and cost structures.

This article is part of the free-to-read Data Science Handbook
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Minimum Cost flow Slotting
The Minimum Cost flow (MCF) problem finds the most cost-effective way to transport a specified amount of flow through a network. This problem appears in many real-world applications, from supply chain logistics and telecommunications routing to resource allocation and scheduling. The MCF problem can model complex network structures while remaining computationally tractable, making it useful in operations research and optimization.
In the context of slotting, the MCF problem helps determine optimal resource allocation across time slots, capacity constraints, and cost structures. For example, in workforce scheduling, we might need to assign employees to different time slots while minimizing costs and respecting availability constraints. In manufacturing, we might need to schedule production across different machines and time periods while minimizing operational costs. The MCF formulation provides a mathematically rigorous framework for solving these allocation problems efficiently.
Unlike simpler optimization approaches that might use greedy heuristics or linear programming, the MCF problem can handle complex network structures with multiple sources, sinks, and intermediate nodes. This makes it particularly valuable for real-world problems where resources flow through multiple stages or where there are complex interdependencies between different allocation decisions. The network structure also allows for natural modeling of capacity constraints, time windows, and cost structures that reflect real operational constraints.
Advantages
The Minimum Cost flow approach offers several advantages for slotting problems. First, it provides a mathematically rigorous framework that guarantees optimal solutions when feasible solutions exist. Unlike heuristic approaches that might find good but suboptimal solutions, the MCF formulation ensures that we find the optimal allocation of resources across the network. This optimality guarantee matters in applications where even small improvements in efficiency can lead to significant cost savings.
Second, the MCF problem is computationally efficient and can handle large-scale problems with thousands of nodes and edges. Modern optimization algorithms, particularly those based on network flow theory, can solve MCF problems very quickly, often in milliseconds or seconds even for complex networks. This computational efficiency makes the approach suitable for real-time applications where decisions need to be made quickly, such as dynamic resource allocation or real-time scheduling systems.
Third, the network structure of the MCF problem provides excellent modeling flexibility. We can easily incorporate various types of constraints, including capacity limits, time windows, precedence relationships, and cost structures. The network representation also makes it intuitive to understand and visualize the problem structure, which is valuable for both model development and communication with stakeholders who need to understand the optimization logic.
Disadvantages
Despite its strengths, the Minimum Cost flow approach has some limitations that practitioners should consider. The most significant limitation is that it requires the problem to be formulated as a network flow problem, which may not always be natural or intuitive for all slotting applications. Some real-world constraints, such as complex precedence relationships or non-linear cost structures, may be difficult to model within the network flow framework.
Another limitation is that the MCF problem assumes that flow can be split and combined freely at nodes, which may not always reflect real-world constraints. For example, in workforce scheduling, we might need to ensure that employees work complete shifts rather than being split across multiple time periods. While these constraints can sometimes be handled through problem transformation, they can make the formulation more complex and less intuitive.
Finally, the MCF approach may struggle with problems that have very tight constraint sets or where finding any feasible solution is difficult. In such cases, the solver might need to explore a large portion of the solution space, leading to longer computation times. Additionally, if the problem is infeasible, the MCF solver will simply report that no solution exists, without providing guidance on which constraints might be relaxed to achieve feasibility.
Formula
To solve the Minimum Cost flow problem mathematically, we need to translate our intuitive understanding of networks, flows, and costs into a precise mathematical framework. This process reveals why each component is needed and how they work together to create a solvable optimization problem.
The Intuition: What Are We Really Trying to Do?
Before diving into mathematical notation, let's clarify what we're trying to achieve. Imagine you're managing a supply chain: you have factories producing goods, warehouses storing them, and markets demanding them. You need to route products through this network in a way that:
- Satisfies all demands - Every market gets exactly what it needs
- Respects all capacities - You can't ship more than your trucks, pipelines, or facilities can handle
- Minimizes total cost - Among all feasible routing strategies, you want the cheapest one
The Minimum Cost flow problem formalizes this intuition. But to solve it mathematically, we need to be precise about what "network," "flow," and "cost" actually mean in mathematical terms.
Building the Network: Nodes and Edges
The first step is representing our problem as a directed graph, a mathematical structure that captures the relationships in our system. Graphs naturally model connections: which locations can send to which other locations, which resources can flow through which pathways.
We define our network as a directed graph where:
- : The directed graph representing our network
- : The set of nodes (vertices) - these represent locations, resources, time periods, or any entities between which flow can occur
- : The set of directed edges (arcs) - these represent the pathways through which flow can travel, with direction indicating the allowed flow direction
The directed nature matters: a factory can ship to a warehouse, but the reverse might not make physical sense. The graph structure captures these one-way relationships naturally.
Characterizing Nodes: Supply, Demand, and Transshipment
Not all nodes play the same role in our network. Some nodes are sources - they generate flow (like factories producing goods). Others are sinks - they consume flow (like markets demanding products). Still others are intermediate nodes - they neither generate nor consume, but simply pass flow through (like warehouses or distribution centers).
We capture this distinction mathematically through a supply value for each node :
where:
- : Node index, where (ranges over all nodes in the network)
- : Supply value at node (units of flow)
- If : Node is a supply node (source) that generates units of flow
- If : Node is a demand node (sink) that requires units of flow
- If : Node is a transshipment node that neither generates nor consumes net flow
Why do we need this distinction? Because all flow needs to originate somewhere and terminate somewhere. The supply values tell us exactly how much flow needs to enter the network (from sources) and how much needs to exit (to sinks). This is the mathematical way of saying "we need to satisfy all demands using available supplies."
Characterizing Edges: Capacity and Cost
Just as nodes have different roles, edges have different properties. Each edge represents a pathway from node to node , and we need to describe two key properties:
where:
- : Directed edge from node to node , where
- : Source node of the edge (where flow originates)
- : Destination node of the edge (where flow terminates)
-
Capacity : The maximum amount of flow that can pass through edge . This represents physical or operational limits - a truck can only carry so much, a pipeline has a maximum throughput, a worker can only handle so many tasks per hour. Units: same as flow units.
-
Cost : The cost per unit of flow sent through edge . This captures the expense, time, or penalty associated with using this particular pathway. Units: cost per unit of flow.
Why do we need both? Capacity constraints prevent us from making physically infeasible allocations (you can't ship 1000 units through a pipeline that can only handle 100). Cost values allow us to distinguish between different feasible pathways. Even if multiple routes can handle the flow, we want to choose the cheapest one.
Decision Variables: What Are We Actually Choosing?
Now we come to the heart of the optimization problem: what decisions are we making? In the Minimum Cost flow problem, our decision is simple but powerful: for each edge in the network, we need to decide how much flow to send through it.
We represent these decisions as decision variables for each edge :
where:
- : Decision variable representing the amount of flow sent through edge (units of flow)
- : Edge from node to node , where
- : "For all" - this constraint applies to every edge in the network
The non-negativity constraint () is needed because flow can only go in the direction of the edge. If we wanted to allow reverse flow, we would need a separate edge pointing in the opposite direction.
These decision variables are what the optimization algorithm will determine. Once we've set up our constraints and objective function, the solver will find the values of all that minimize cost while satisfying all constraints.
The Objective: Minimizing Total Cost
Our goal is to find the cheapest way to route flow through the network. But what does "cheapest" mean mathematically?
The total cost of a solution is the sum of costs across all edges, where each edge's contribution is its unit cost multiplied by the amount of flow we send through it. This gives us our objective function:
where:
- : Minimization operator - we seek to minimize the total cost
- : Sum over all edges in the edge set
- : Unit cost per flow on edge (cost per unit of flow)
- : Flow amount on edge (units of flow)
- : Total cost contribution from edge (cost units)
This formula assumes that costs are linear in the amount of flow. Sending 10 units through an edge costs exactly 10 times as much as sending 1 unit. This linearity makes the problem computationally tractable, and it's a reasonable assumption for many real-world scenarios (though not all; some problems require non-linear cost functions, which would need different solution approaches).
The minimization operator tells us we're seeking the solution that makes this sum as small as possible, subject to all the constraints we're about to define.
Flow Conservation: The Fundamental Law
We need to ensure that flow is conserved throughout the network. Flow doesn't magically appear or disappear at nodes. This conservation principle expresses physical reality: what goes into a node needs to come out (or be consumed), and what comes out needs to have come in (or been generated).
For each node , we write the flow conservation constraint:
Let's visualize what this means at a single node:

This visualization shows how flow conservation works at a single node. Blue arrows represent flow entering the node, green arrows represent flow leaving the node. The equation Flow Out - Flow In = Supply must hold for each node type.
where:
- : Node index, where (this constraint applies to every node)
- : Sum of all flow leaving node (flow on edges that start at )
- : Index over all nodes such that edge exists
- : Flow amount on edge leaving node
- : Sum of all flow entering node (flow on edges that end at )
- : Index over all nodes such that edge exists
- : Flow amount on edge entering node
- : Supply value at node (positive for sources, negative for sinks, zero for transshipment)
Let's break this down carefully:
- The first sum represents all flow leaving node (flow on edges that start at )
- The second sum represents all flow entering node (flow on edges that end at )
- Their difference equals , the supply at node
Why is this the right constraint? Consider what happens at different types of nodes:
-
Supply nodes (): More flow leaves than enters. The difference equals the supply generated at this node. For example, if a factory generates 10 units, then , meaning 10 more units leave than enter.
-
Demand nodes (): More flow enters than leaves. The difference is negative, matching the negative demand. For example, if a market needs 10 units, then , meaning 10 more units enter than leave.
-
Transshipment nodes (): Flow in equals flow out. These nodes simply pass flow through without generating or consuming any net amount.
This single constraint type elegantly handles all three node types, ensuring that flow is properly balanced throughout the entire network. Without this constraint, we could have solutions where flow appears or disappears at nodes, which would be physically meaningless.
Capacity Constraints: Respecting Physical Limits
While flow conservation ensures flow is balanced, it doesn't prevent us from trying to send more flow through an edge than it can physically handle. We need capacity constraints to enforce the limits we defined for each edge:
where:
- : Flow amount on edge (units of flow)
- : Capacity (maximum flow) allowed on edge (units of flow)
- : "For all edges" - this constraint applies to every edge in the network
- : Non-negativity constraint (flow cannot be negative)
- : Capacity constraint (flow cannot exceed the edge capacity)
This constraint specifies that the flow on edge should be:
- At least zero (we already had this from the non-negativity of decision variables)
- At most (the capacity limit we defined for this edge)
Why is this necessary? Without capacity constraints, the optimal solution might try to route all flow through a single cheap edge, even if that edge can only handle a fraction of the required flow. Capacity constraints force the solver to distribute flow across multiple pathways, respecting the physical realities of the network.
Together with flow conservation, capacity constraints ensure that our solution is not just mathematically valid, but also physically feasible.
The Complete Mathematical Formulation
Now we can assemble all these pieces into the complete Minimum Cost flow problem:
Objective Function:
Subject to:
Flow Conservation Constraints:
Capacity Constraints:
Variable Definitions:
- : Directed graph with node set and edge set
- : Node indices, where
- : Directed edge from node to node , where
- : Decision variable - flow amount on edge (units of flow)
- : Unit cost per flow on edge (cost per unit of flow)
- : Capacity (maximum flow) on edge (units of flow)
- : Supply value at node (positive for sources, negative for sinks, zero for transshipment)
- : "For all" quantifier
This formulation is a linear programming problem, a special type of optimization problem where both the objective function and all constraints are linear in the decision variables. This linearity allows us to use highly efficient specialized algorithms that can solve even large-scale problems quickly.
Why This Formulation Works: Key Mathematical Properties
The Minimum Cost flow formulation has several beautiful mathematical properties that make it both theoretically elegant and practically useful:
-
Linear Programming Structure: The MCF problem is a special case of linear programming with a particularly nice structure. The network topology creates a constraint matrix with special properties (totally unimodular), which enables the use of highly efficient specialized algorithms that are much faster than general-purpose linear programming solvers.
-
Integrality Property: If all supplies , demands, and capacities are integers, then there exists an optimal solution where all flows are also integers. This matters for practical applications where fractional allocations don't make sense; you can't ship 3.7 trucks or assign 2.3 workers to a shift. The integrality property guarantees that we'll get whole-number solutions automatically, without needing to add integer constraints (which would make the problem much harder to solve).
-
Duality and Economic Interpretation: The MCF problem has a natural dual interpretation involving node potentials (often interpreted as prices). The dual variables tell us the marginal value of increasing supply or capacity at different nodes, providing valuable economic insights. If the dual variable at a demand node is high, it means that location is a bottleneck - increasing supply there would significantly reduce total cost.
These properties aren't just mathematical curiosities - they're what make the Minimum Cost flow problem both solvable and interpretable in real-world applications. The linear structure enables fast computation, the integrality property ensures practical solutions, and the duality provides insights for decision-makers.
Visualizing Minimum Cost flow
Let's create visualizations to understand how the Minimum Cost flow problem works in practice. We'll start by setting up the necessary libraries and creating a simple example.

Now let's solve this network using OR-Tools and visualize the optimal flow:
1def solve_mcf_ortools(G):
2 """solve the MCF problem using OR-Tools"""
3 # Create the solver
4 solver = min_cost_flow.SimpleMinCostFlow()
5
6 # Extract network data
7 start_nodes = []
8 end_nodes = []
9 capacities = []
10 unit_costs = []
11 supplies = []
12
13 # Add edges
14 for (u, v) in G.edges():
15 start_nodes.append(u)
16 end_nodes.append(v)
17 capacities.append(G[u][v]['capacity'])
18 unit_costs.append(G[u][v]['cost'])
19
20 # Add supplies
21 for node in G.nodes():
22 supplies.append(G.nodes[node]['supply'])
23
24 # Add arcs to the solver
25 for i in range(len(start_nodes)):
26 solver.add_arc_with_capacity_and_unit_cost(
27 start_nodes[i], end_nodes[i], capacities[i], unit_costs[i])
28
29 # Add node supplies
30 for i in range(len(supplies)):
31 solver.set_node_supply(i, supplies[i])
32
33 # solve
34 status = solver.solve()
35
36 if status == solver.OPTIMAL:
37 print(f"Minimum cost: {solver.optimal_cost()}")
38 print("\nOptimal flow:")
39 print("Arc flow / capacity Cost")
40
41 flows = {}
42 for i in range(solver.num_arcs()):
43 tail = solver.tail(i)
44 head = solver.head(i)
45 flow = solver.flow(i)
46 capacity = solver.capacity(i)
47 cost = solver.flow(i) * solver.unit_cost(i)
48
49 flows[(tail, head)] = flow
50 print(f"{tail} -> {head} {flow} / {capacity} {cost}")
51
52 return flows, solver.optimal_cost()
53 else:
54 print("No optimal solution found")
55 return None, None
56
57# solve the problem
58flows, total_cost = solve_mcf_ortools(G)1def solve_mcf_ortools(G):
2 """solve the MCF problem using OR-Tools"""
3 # Create the solver
4 solver = min_cost_flow.SimpleMinCostFlow()
5
6 # Extract network data
7 start_nodes = []
8 end_nodes = []
9 capacities = []
10 unit_costs = []
11 supplies = []
12
13 # Add edges
14 for (u, v) in G.edges():
15 start_nodes.append(u)
16 end_nodes.append(v)
17 capacities.append(G[u][v]['capacity'])
18 unit_costs.append(G[u][v]['cost'])
19
20 # Add supplies
21 for node in G.nodes():
22 supplies.append(G.nodes[node]['supply'])
23
24 # Add arcs to the solver
25 for i in range(len(start_nodes)):
26 solver.add_arc_with_capacity_and_unit_cost(
27 start_nodes[i], end_nodes[i], capacities[i], unit_costs[i])
28
29 # Add node supplies
30 for i in range(len(supplies)):
31 solver.set_node_supply(i, supplies[i])
32
33 # solve
34 status = solver.solve()
35
36 if status == solver.OPTIMAL:
37 print(f"Minimum cost: {solver.optimal_cost()}")
38 print("\nOptimal flow:")
39 print("Arc flow / capacity Cost")
40
41 flows = {}
42 for i in range(solver.num_arcs()):
43 tail = solver.tail(i)
44 head = solver.head(i)
45 flow = solver.flow(i)
46 capacity = solver.capacity(i)
47 cost = solver.flow(i) * solver.unit_cost(i)
48
49 flows[(tail, head)] = flow
50 print(f"{tail} -> {head} {flow} / {capacity} {cost}")
51
52 return flows, solver.optimal_cost()
53 else:
54 print("No optimal solution found")
55 return None, None
56
57# solve the problem
58flows, total_cost = solve_mcf_ortools(G)Minimum cost: 60 Optimal flow: Arc flow / capacity Cost 0 -> 1 10 / 15 40 0 -> 2 0 / 8 0 1 -> 3 10 / 10 20 1 -> 2 0 / 5 0 2 -> 3 0 / 12 0

Example
Let's work through a concrete example that brings together all the concepts we've discussed. We'll see how the mathematical formulation translates into a real optimization problem, and we'll trace through the logic to understand why the optimal solution takes the form it does.
Setting Up the Problem: A Supply Chain Scenario
Imagine you're managing a supply chain with the following structure:
- Factory (Node 0): Produces 10 units of product (a supply node with )
- Warehouse (Node 1): A storage facility that neither produces nor consumes (a transshipment node with )
- Distribution Center (Node 2): Another intermediate facility (a transshipment node with )
- Market (Node 3): Requires exactly 10 units to satisfy demand (a demand node with )
The network has five possible shipping routes, each with different capacities and costs:
- Factory → Warehouse: Can handle up to 15 units, costs 4 per unit
- Factory → Distribution: Can handle up to 8 units, costs 6 per unit
- Warehouse → Market: Can handle up to 10 units, costs 2 per unit
- Distribution → Market: Can handle up to 12 units, costs 3 per unit
- Warehouse → Distribution: Can handle up to 5 units, costs 1 per unit (a shortcut route)
Notice that we have multiple pathways from the factory to the market. Our goal is to find the cheapest way to route all 10 units from the factory to the market while respecting capacity limits.
Here's a visual representation of this network:

This diagram shows the network structure we'll analyze. The Factory (node 0) supplies 10 units, the Market (node 3) demands 10 units, and the Warehouse and Distribution Center are transshipment nodes. Each edge is labeled with its unit cost and capacity limit.
Translating to Mathematical Formulation
Step 1: Define Decision Variables
We need to decide how much flow to send through each of the five edges:
- = flow from Factory to Warehouse
- = flow from Factory to Distribution
- = flow from Warehouse to Market
- = flow from Distribution to Market
- = flow from Warehouse to Distribution
Each of these should be non-negative (we can't send negative flow), and we'll determine their optimal values through optimization.
Step 2: Construct the Objective Function
Our goal is to minimize total cost. The objective function follows the general form:
For our specific network, this expands to:
Substituting the unit costs:
where:
- : Unit cost for Factory → Warehouse
- : Unit cost for Factory → Distribution
- : Unit cost for Warehouse → Market
- : Unit cost for Distribution → Market
- : Unit cost for Warehouse → Distribution
Each term represents the cost contribution from one edge: the unit cost multiplied by the flow amount. Notice that the Warehouse → Distribution route () has the lowest unit cost (1), which might make it attractive if we can use it effectively.
Step 3: Apply Flow Conservation Constraints
Flow conservation ensures that flow is balanced at each node. For each node , we apply the general form:
Let's work through each node:
-
Factory (Node 0): This is a supply node with . Applying the flow conservation constraint:
Simplified:
This says: all flow leaving the factory (the two outgoing edges) must equal the 10 units the factory produces. There are no incoming edges to the factory, so this is simply a supply constraint.
-
Warehouse (Node 1): This is a transshipment node with . Applying the flow conservation constraint:
Simplified: , or equivalently
This says: all flow entering the warehouse (from the factory) must equal all flow leaving (to the market and to the distribution center). The warehouse neither creates nor destroys flow.
-
Distribution (Node 2): This is also a transshipment node with . Applying the flow conservation constraint:
Simplified: , or equivalently
This says: flow entering the distribution center (from the factory and from the warehouse) must equal flow leaving (to the market).
-
Market (Node 3): This is a demand node with . Applying the flow conservation constraint:
Simplified: , or equivalently
This says: all flow entering the market (from the warehouse and from the distribution center) must equal the 10 units of demand.
Step 4: Apply Capacity Constraints
Each edge has a maximum capacity that cannot be exceeded. The general form is:
For our specific network, this gives us:
- (Factory → Warehouse)
- (Factory → Distribution)
- (Warehouse → Market)
- (Distribution → Market)
- (Warehouse → Distribution)
where:
- : Capacity of Factory → Warehouse edge
- : Capacity of Factory → Distribution edge
- : Capacity of Warehouse → Market edge
- : Capacity of Distribution → Market edge
- : Capacity of Warehouse → Distribution edge
These constraints prevent us from overloading any shipping route.
Finding the Optimal Solution
Now we have a complete optimization problem. Let's reason through what the optimal solution should look like, then verify with computation.
Intuitive Reasoning:
The cheapest route from Factory to Market is Factory → Warehouse → Market, with total cost per unit. However, the Warehouse → Market edge has capacity 10, which is exactly our demand. So we could send all 10 units this way... but wait, let's check if there's a better strategy.
Actually, we have an interesting option: the Warehouse → Distribution route costs only 1 per unit, and Distribution → Market costs 3 per unit. So Factory → Warehouse → Distribution → Market costs per unit, which is more expensive than the direct Warehouse → Market route.
However, the Factory → Distribution route costs 6 per unit, and Distribution → Market costs 3 per unit, for a total of per unit. This is the most expensive direct path.
The optimal strategy balances these trade-offs. Since Warehouse → Market has capacity 10 (exactly our demand), we could use it fully. But we also need to consider whether splitting flow might be beneficial.
Finding the Optimal Solution:
Now we have a complete optimization problem. Let's think through the solution strategy:
Strategy 1: Use the cheapest path exclusively
The path Factory → Warehouse → Market has total cost per unit calculated as:
If we send all 10 units this way:
- (within capacity )
- (exactly at capacity )
Total cost calculation:
This is feasible and seems attractive. But is it optimal?
Strategy 2: Split between two direct paths
We could send some units Factory → Warehouse → Market and some Factory → Distribution → Market:
- Factory → Warehouse → Market: cost per unit = cost units per flow unit
- Factory → Distribution → Market: cost per unit = cost units per flow unit
Since the first path is cheaper, we'd want to use it to full capacity (10 units), leaving 0 for the second path. This gives us Strategy 1 again.
Strategy 3: Use the shortcut route
What about using the Warehouse → Distribution shortcut? The path Factory → Warehouse → Distribution → Market has cost per unit:
This is more expensive than the direct Warehouse → Market route (cost 6 per unit). So this doesn't help.
The Optimal Solution:
By solving the optimization problem (either algebraically or using a solver), we find that Strategy 1 is optimal:
- (all 10 units: Factory → Warehouse)
- (all 10 units: Warehouse → Market)
- , , (unused routes)
Total Cost Calculation:
The total cost is computed using the objective function:
Why This Makes Sense:
This solution is optimal because:
- The cheapest path (Factory → Warehouse → Market) has sufficient capacity to handle all demand
- Using it exclusively minimizes cost at 6 per unit
- All alternative paths cost more (9 per unit for Factory → Distribution → Market, 8 per unit if we use the shortcut)
The key insight is that when the cheapest path has sufficient capacity, we should use it exclusively. The solver would only split flow across multiple paths if:
- No single path has enough capacity for all demand, OR
- There are additional constraints (like minimum flow requirements) that force splitting, OR
- The cost structure makes splitting beneficial (which doesn't happen in this example)
Verifying the Constraints:
Let's verify this solution satisfies all constraints:
Flow Conservation Constraints:
For each node , we verify that :
-
Factory (Node 0): Flow out - Flow in = Supply
-
Warehouse (Node 1): Flow out - Flow in = Supply
-
Distribution (Node 2): Flow out - Flow in = Supply
-
Market (Node 3): Flow out - Flow in = Supply
Capacity Constraints:
For each edge , we verify that :
- :
- :
- : (at capacity)
- :
- :
All constraints are satisfied, and we've achieved the minimum possible cost. This example illustrates how the mathematical formulation guides us to the optimal solution systematically, even when the problem seems simple enough to solve by intuition.
Implementation in OR-Tools
Now that we understand the mathematical formulation, let's see how to implement it in practice. We'll use OR-Tools, Google's open-source optimization library, which provides efficient solvers specifically designed for network flow problems.
Why OR-Tools?
OR-Tools is an excellent choice for Minimum Cost flow problems because:
-
Specialized Algorithms: The
SimpleMinCostFlowsolver uses highly optimized algorithms (like the successive shortest path algorithm or cost scaling) that are much faster than general-purpose linear programming solvers for network flow problems. -
Ease of Use: The API directly maps to our mathematical formulation - we specify nodes, edges, capacities, costs, and supplies, and the solver handles the rest.
-
Robustness: OR-Tools has been battle-tested on real-world problems and handles edge cases (infeasible problems, numerical issues) gracefully.
-
Open Source: Free to use and well-documented, making it accessible for both learning and production applications.
Mapping Mathematics to Code
The key insight is that our mathematical formulation translates directly into OR-Tools API calls:
- Edges →
add_arc_with_capacity_and_unit_cost(start_node, end_node, capacity, cost) - Node Supplies →
set_node_supply(node, supply) - Decision Variables → Automatically handled by the solver (we query the results)
- Objective Function → Automatically minimized by the solver
- Constraints → Automatically enforced by the solver
The solver takes care of all the constraint enforcement and optimization - we just need to specify the problem structure.
Building a Reusable Solver Function
Let's create a function that encapsulates the solution process, making it easy to solve different MCF problems. This function will handle network construction, solving, and solution extraction:
1from ortools.graph.python import min_cost_flow
2import numpy as np
3import pandas as pd
4
5def solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies):
6 """
7 Solve a minimum cost flow problem using OR-Tools.
8
9 This function translates our mathematical formulation into OR-Tools API calls:
10 - Edges (i,j) with capacity u_ij and cost c_ij → add_arc_with_capacity_and_unit_cost()
11 - Node supplies b_i → set_node_supply()
12 - The solver automatically handles flow conservation, capacity constraints, and optimization
13
14 Parameters:
15 -----------
16 start_nodes : list
17 List of starting nodes for each arc (the "i" in edge (i,j))
18 end_nodes : list
19 List of ending nodes for each arc (the "j" in edge (i,j))
20 capacities : list
21 List of capacities u_ij for each arc (upper bounds on flow)
22 unit_costs : list
23 List of unit costs c_ij for each arc (cost per unit of flow)
24 supplies : list
25 List of supplies b_i for each node (positive for sources, negative for sinks, zero for transshipment)
26
27 Returns:
28 --------
29 dict : Dictionary with solution information including optimal flows and total cost
30 """
31 # Create the solver instance
32 # This initializes the internal data structures that will represent our network
33 solver = min_cost_flow.SimpleMinCostFlow()
34
35 # Step 1: Add all edges to the network
36 # This corresponds to defining the set E in our graph G = (V, E)
37 # For each edge, we specify: start node, end node, capacity (u_ij), and unit cost (c_ij)
38 for i in range(len(start_nodes)):
39 solver.add_arc_with_capacity_and_unit_cost(
40 start_nodes[i], end_nodes[i], capacities[i], unit_costs[i])
41
42 # Step 2: Set node supplies/demands
43 # This corresponds to setting the values b_i for each node i ∈ V
44 # The solver will use these to enforce flow conservation constraints
45 for i in range(len(supplies)):
46 solver.set_node_supply(i, supplies[i])
47
48 # Step 3: Solve the optimization problem
49 # The solver automatically:
50 # - Enforces flow conservation: Σx_out - Σx_in = b_i for each node
51 # - Enforces capacity constraints: 0 ≤ x_ij ≤ u_ij for each edge
52 # - Minimizes the objective: min Σ c_ij * x_ij
53 status = solver.solve()
54
55 # Step 4: Extract and return the solution
56 if status == solver.OPTIMAL:
57 # The solver found an optimal solution
58 solution = {
59 'status': 'OPTIMAL',
60 'total_cost': solver.optimal_cost(), # The value of our minimized objective function
61 'flows': {}, # Dictionary mapping (start, end) → flow amount x_ij
62 'arc_info': [] # List of detailed information for each arc
63 }
64
65 # Extract the optimal flow value for each edge
66 # These are the values of our decision variables x_ij that minimize cost
67 for i in range(solver.num_arcs()):
68 tail = solver.tail(i) # Start node of this arc
69 head = solver.head(i) # End node of this arc
70 flow = solver.flow(i) # Optimal flow x_ij on this arc
71 capacity = solver.capacity(i) # Capacity u_ij of this arc
72 unit_cost = solver.unit_cost(i) # Unit cost c_ij of this arc
73 arc_cost = flow * unit_cost # Total cost contribution: c_ij * x_ij
74
75 # Store the flow in our solution dictionary
76 solution['flows'][(tail, head)] = flow
77 solution['arc_info'].append({
78 'arc': (tail, head),
79 'flow': flow,
80 'capacity': capacity,
81 'unit_cost': unit_cost,
82 'total_cost': arc_cost
83 })
84
85 return solution
86 else:
87 # The problem is infeasible (no solution exists) or the solver encountered an error
88 return {
89 'status': 'INFEASIBLE' if status == solver.INFEASIBLE else 'UNKNOWN',
90 'total_cost': None,
91 'flows': {},
92 'arc_info': []
93 }
94
95# Example: Supply chain optimization
96def supply_chain_example():
97 """
98 Example of supply chain optimization using MCF.
99
100 This implements the supply chain problem we worked through in the Example section:
101 - Factory (node 0) produces 10 units (supply = 10)
102 - Warehouse (node 1) and Distribution (node 2) are transshipment nodes (supply = 0)
103 - Market (node 3) requires 10 units (supply = -10, which represents demand)
104
105 The network has 5 edges representing different shipping routes, each with
106 capacity limits and transportation costs. The solver will find the cheapest
107 way to route all 10 units from factory to market.
108 """
109
110 # Define the network structure
111 # Each list has the same length, with index i representing the i-th edge
112 start_nodes = [0, 0, 1, 1, 2] # From nodes: edges start at these nodes
113 end_nodes = [1, 2, 2, 3, 3] # To nodes: edges end at these nodes
114 # Together, these define edges: (0,1), (0,2), (1,2), (1,3), (2,3)
115
116 # Capacity constraints: u_ij for each edge
117 # These are the upper bounds that prevent overloading any route
118 capacities = [15, 8, 5, 10, 12] # Maximum flow allowed on each edge
119
120 # Unit costs: c_ij for each edge
121 # These determine the cost per unit of flow, used in the objective function
122 unit_costs = [4, 6, 1, 2, 3] # Cost per unit of flow on each edge
123
124 # Node supplies: b_i for each node
125 # Positive = supply (source), negative = demand (sink), zero = transshipment
126 supplies = [10, 0, 0, -10] # Factory supplies 10, Market demands 10
127
128 # Solve the optimization problem
129 # This function call translates our problem into the mathematical formulation
130 # and uses OR-Tools to find the optimal flow values x_ij
131 solution = solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies)
132
133 return solution
134
135# Run the example
136# This solves the supply chain problem we analyzed mathematically earlier
137supply_chain_solution = supply_chain_example()1from ortools.graph.python import min_cost_flow
2import numpy as np
3import pandas as pd
4
5def solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies):
6 """
7 Solve a minimum cost flow problem using OR-Tools.
8
9 This function translates our mathematical formulation into OR-Tools API calls:
10 - Edges (i,j) with capacity u_ij and cost c_ij → add_arc_with_capacity_and_unit_cost()
11 - Node supplies b_i → set_node_supply()
12 - The solver automatically handles flow conservation, capacity constraints, and optimization
13
14 Parameters:
15 -----------
16 start_nodes : list
17 List of starting nodes for each arc (the "i" in edge (i,j))
18 end_nodes : list
19 List of ending nodes for each arc (the "j" in edge (i,j))
20 capacities : list
21 List of capacities u_ij for each arc (upper bounds on flow)
22 unit_costs : list
23 List of unit costs c_ij for each arc (cost per unit of flow)
24 supplies : list
25 List of supplies b_i for each node (positive for sources, negative for sinks, zero for transshipment)
26
27 Returns:
28 --------
29 dict : Dictionary with solution information including optimal flows and total cost
30 """
31 # Create the solver instance
32 # This initializes the internal data structures that will represent our network
33 solver = min_cost_flow.SimpleMinCostFlow()
34
35 # Step 1: Add all edges to the network
36 # This corresponds to defining the set E in our graph G = (V, E)
37 # For each edge, we specify: start node, end node, capacity (u_ij), and unit cost (c_ij)
38 for i in range(len(start_nodes)):
39 solver.add_arc_with_capacity_and_unit_cost(
40 start_nodes[i], end_nodes[i], capacities[i], unit_costs[i])
41
42 # Step 2: Set node supplies/demands
43 # This corresponds to setting the values b_i for each node i ∈ V
44 # The solver will use these to enforce flow conservation constraints
45 for i in range(len(supplies)):
46 solver.set_node_supply(i, supplies[i])
47
48 # Step 3: Solve the optimization problem
49 # The solver automatically:
50 # - Enforces flow conservation: Σx_out - Σx_in = b_i for each node
51 # - Enforces capacity constraints: 0 ≤ x_ij ≤ u_ij for each edge
52 # - Minimizes the objective: min Σ c_ij * x_ij
53 status = solver.solve()
54
55 # Step 4: Extract and return the solution
56 if status == solver.OPTIMAL:
57 # The solver found an optimal solution
58 solution = {
59 'status': 'OPTIMAL',
60 'total_cost': solver.optimal_cost(), # The value of our minimized objective function
61 'flows': {}, # Dictionary mapping (start, end) → flow amount x_ij
62 'arc_info': [] # List of detailed information for each arc
63 }
64
65 # Extract the optimal flow value for each edge
66 # These are the values of our decision variables x_ij that minimize cost
67 for i in range(solver.num_arcs()):
68 tail = solver.tail(i) # Start node of this arc
69 head = solver.head(i) # End node of this arc
70 flow = solver.flow(i) # Optimal flow x_ij on this arc
71 capacity = solver.capacity(i) # Capacity u_ij of this arc
72 unit_cost = solver.unit_cost(i) # Unit cost c_ij of this arc
73 arc_cost = flow * unit_cost # Total cost contribution: c_ij * x_ij
74
75 # Store the flow in our solution dictionary
76 solution['flows'][(tail, head)] = flow
77 solution['arc_info'].append({
78 'arc': (tail, head),
79 'flow': flow,
80 'capacity': capacity,
81 'unit_cost': unit_cost,
82 'total_cost': arc_cost
83 })
84
85 return solution
86 else:
87 # The problem is infeasible (no solution exists) or the solver encountered an error
88 return {
89 'status': 'INFEASIBLE' if status == solver.INFEASIBLE else 'UNKNOWN',
90 'total_cost': None,
91 'flows': {},
92 'arc_info': []
93 }
94
95# Example: Supply chain optimization
96def supply_chain_example():
97 """
98 Example of supply chain optimization using MCF.
99
100 This implements the supply chain problem we worked through in the Example section:
101 - Factory (node 0) produces 10 units (supply = 10)
102 - Warehouse (node 1) and Distribution (node 2) are transshipment nodes (supply = 0)
103 - Market (node 3) requires 10 units (supply = -10, which represents demand)
104
105 The network has 5 edges representing different shipping routes, each with
106 capacity limits and transportation costs. The solver will find the cheapest
107 way to route all 10 units from factory to market.
108 """
109
110 # Define the network structure
111 # Each list has the same length, with index i representing the i-th edge
112 start_nodes = [0, 0, 1, 1, 2] # From nodes: edges start at these nodes
113 end_nodes = [1, 2, 2, 3, 3] # To nodes: edges end at these nodes
114 # Together, these define edges: (0,1), (0,2), (1,2), (1,3), (2,3)
115
116 # Capacity constraints: u_ij for each edge
117 # These are the upper bounds that prevent overloading any route
118 capacities = [15, 8, 5, 10, 12] # Maximum flow allowed on each edge
119
120 # Unit costs: c_ij for each edge
121 # These determine the cost per unit of flow, used in the objective function
122 unit_costs = [4, 6, 1, 2, 3] # Cost per unit of flow on each edge
123
124 # Node supplies: b_i for each node
125 # Positive = supply (source), negative = demand (sink), zero = transshipment
126 supplies = [10, 0, 0, -10] # Factory supplies 10, Market demands 10
127
128 # Solve the optimization problem
129 # This function call translates our problem into the mathematical formulation
130 # and uses OR-Tools to find the optimal flow values x_ij
131 solution = solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies)
132
133 return solution
134
135# Run the example
136# This solves the supply chain problem we analyzed mathematically earlier
137supply_chain_solution = supply_chain_example()Now let's display the results from the supply chain example:
Supply Chain Network: Nodes: Factory(0), Warehouse(1), Distribution(2), Market(3) Supplies/Demands: [10, 0, 0, -10] (Positive = supply, Negative = demand, Zero = transshipment) Optimal solution found! Total cost: 60.0 (This is the minimized value of Σ c_ij * x_ij) Optimal flow on each arc: Arc Flow / Capacity Unit Cost Total Cost ------------------------------------------------------- 0 -> 1 10 / 15 4.0 40.0 0 -> 2 0 / 8 6.0 0.0 1 -> 2 0 / 5 1.0 0.0 1 -> 3 10 / 10 2.0 20.0 2 -> 3 0 / 12 3.0 0.0 Note: Only edges with positive flow are used in the optimal solution. The solver automatically set unused edges to zero flow.
The solver found an optimal solution with a total cost of 60 cost units. This matches our mathematical analysis from the Example section, where we determined that routing all 10 units through Factory → Warehouse → Market (cost 6 per unit) is optimal. The solution shows that edges (0,1) and (1,3) carry the full flow of 10 units each, while other edges remain unused. This makes operational sense: the cheapest path has sufficient capacity to handle all demand, so there's no need to split flow across multiple routes. The unused edges (0,2), (1,2), and (2,3) have zero flow, indicating they're not part of the optimal solution.
A More Complex Example: Time Slotting
The power of the Minimum Cost flow formulation becomes even clearer when we tackle more complex problems. Let's consider a time slotting problem where we need to allocate resources across multiple time periods to complete various tasks. This demonstrates how MCF can model problems with multiple stages and complex routing requirements.
The key insight is that we can model time as a dimension in our network: resources flow through time slots, and then through tasks, creating a multi-stage network structure.
1def time_slotting_example():
2 """
3 Example of resource allocation across time slots using MCF.
4
5 This demonstrates how MCF can model slotting problems where resources
6 must be allocated across time periods. The network structure is:
7
8 Source → Resources → Time Slots → Tasks → Sink
9
10 This creates a multi-stage flow where:
11 1. Resources are allocated from a source
12 2. Resources are assigned to time slots (with costs that may vary by time)
13 3. Time slots are connected to tasks (with costs that may vary by task priority)
14 4. Tasks flow to a sink (representing completed work)
15
16 The solver finds the optimal allocation that minimizes total cost while
17 respecting capacity constraints at each stage.
18 """
19
20 # Define the problem dimensions
21 time_slots = 3 # We have 3 time periods to allocate resources
22 resources = 2 # We have 2 resources (e.g., workers, machines)
23 tasks = 4 # We need to complete 4 tasks
24
25 # Build the network structure layer by layer
26 # This creates a multi-stage network: Source → Resources → Time Slots → Tasks → Sink
27 start_nodes = []
28 end_nodes = []
29 capacities = []
30 unit_costs = []
31
32 # Stage 1: Source to Resources
33 # The source node (node 0) represents the total available capacity
34 # We connect it to each resource, allowing flow to be allocated to different resources
35 source = 0
36 for r in range(resources):
37 start_nodes.append(source)
38 end_nodes.append(r + 1) # Resource nodes are numbered 1, 2, ...
39 capacities.append(10) # Each resource has capacity to handle 10 units total
40 unit_costs.append(0) # No cost for allocating resources (costs occur when using them)
41
42 # Stage 2: Resources to Time Slots
43 # Each resource can be assigned to work in different time slots
44 # This creates edges from each resource to each time slot
45 # Node numbering: resources are 1..resources, time slots are (resources+1)..(resources+time_slots)
46 for r in range(resources):
47 for t in range(time_slots):
48 start_nodes.append(r + 1) # From resource r
49 end_nodes.append(resources + 1 + t) # To time slot t
50 capacities.append(5) # Each resource can work 5 units per time slot (capacity constraint)
51 unit_costs.append(1) # Base cost for a resource working in a time slot
52
53 # Stage 3: Time Slots to Tasks
54 # Each time slot can contribute to completing different tasks
55 # This creates edges from each time slot to each task
56 # Node numbering: tasks are (resources+time_slots+1)..(resources+time_slots+tasks)
57 for t in range(time_slots):
58 for task in range(tasks):
59 start_nodes.append(resources + 1 + t) # From time slot t
60 end_nodes.append(resources + time_slots + 1 + task) # To task
61 capacities.append(3) # Each time slot can handle 3 units per task
62 unit_costs.append(t + 1) # Cost increases with later time slots (urgency penalty)
63 # This models that completing work earlier is preferred
64
65 # Stage 4: Tasks to Sink
66 # The sink node represents completed work
67 # All tasks must flow to the sink, representing that all tasks must be completed
68 sink = resources + time_slots + tasks + 1
69 for task in range(tasks):
70 start_nodes.append(resources + time_slots + 1 + task) # From task
71 end_nodes.append(sink) # To sink
72 capacities.append(8) # Each task can accept up to 8 units of work
73 unit_costs.append(0) # No cost for completing tasks (costs are in the process)
74
75 # Define node supplies/demands
76 # This enforces that:
77 # - Source supplies total capacity (20 units)
78 # - All intermediate nodes are transshipment (supply = 0)
79 # - Sink demands the same amount (20 units) to ensure all work is completed
80 supplies = [20] + [0] * (resources + time_slots + tasks) + [-20]
81
82 # Solve the optimization problem
83 # The solver will find the optimal way to route flow through all stages
84 # while minimizing total cost and respecting all capacity constraints
85 solution = solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies)
86
87 # Analyze the solution by aggregating flows at each stage
88 # This helps us understand how the solver allocated resources
89 resource_usage = {} # How much each resource is used
90 time_slot_usage = {} # How much work is done in each time slot
91 task_usage = {} # How much work is allocated to each task
92
93 # Parse the solution to extract flow information
94 # We examine each edge in the solution and aggregate flows by stage
95 if solution['status'] == 'OPTIMAL':
96 for arc_info in solution['arc_info']:
97 arc = arc_info['arc']
98 flow = arc_info['flow'] # The optimal flow x_ij on this edge
99
100 if flow > 0: # Only consider edges that are actually used
101 start, end = arc
102
103 # Stage 1: Source to Resources
104 # Track how much flow is allocated to each resource
105 if start == 0: # Edge starts at source
106 resource_usage[end] = resource_usage.get(end, 0) + flow
107
108 # Stage 2: Resources to Time Slots
109 # Track how much work each resource does in each time slot
110 elif 1 <= start <= resources: # Edge starts at a resource
111 time_slot = end - resources - 1 # Extract time slot index
112 time_slot_usage[time_slot] = time_slot_usage.get(time_slot, 0) + flow
113
114 # Stage 3: Time Slots to Tasks
115 # Track how much work each time slot contributes to each task
116 elif resources + 1 <= start <= resources + time_slots: # Edge starts at a time slot
117 task = end - resources - time_slots - 1 # Extract task index
118 task_usage[task] = task_usage.get(task, 0) + flow
119
120 return solution, resource_usage, time_slot_usage, task_usage, resources, time_slots, tasks
121
122# Run the time slotting example
123time_slotting_solution, resource_usage, time_slot_usage, task_usage, num_resources, num_time_slots, num_tasks = time_slotting_example()1def time_slotting_example():
2 """
3 Example of resource allocation across time slots using MCF.
4
5 This demonstrates how MCF can model slotting problems where resources
6 must be allocated across time periods. The network structure is:
7
8 Source → Resources → Time Slots → Tasks → Sink
9
10 This creates a multi-stage flow where:
11 1. Resources are allocated from a source
12 2. Resources are assigned to time slots (with costs that may vary by time)
13 3. Time slots are connected to tasks (with costs that may vary by task priority)
14 4. Tasks flow to a sink (representing completed work)
15
16 The solver finds the optimal allocation that minimizes total cost while
17 respecting capacity constraints at each stage.
18 """
19
20 # Define the problem dimensions
21 time_slots = 3 # We have 3 time periods to allocate resources
22 resources = 2 # We have 2 resources (e.g., workers, machines)
23 tasks = 4 # We need to complete 4 tasks
24
25 # Build the network structure layer by layer
26 # This creates a multi-stage network: Source → Resources → Time Slots → Tasks → Sink
27 start_nodes = []
28 end_nodes = []
29 capacities = []
30 unit_costs = []
31
32 # Stage 1: Source to Resources
33 # The source node (node 0) represents the total available capacity
34 # We connect it to each resource, allowing flow to be allocated to different resources
35 source = 0
36 for r in range(resources):
37 start_nodes.append(source)
38 end_nodes.append(r + 1) # Resource nodes are numbered 1, 2, ...
39 capacities.append(10) # Each resource has capacity to handle 10 units total
40 unit_costs.append(0) # No cost for allocating resources (costs occur when using them)
41
42 # Stage 2: Resources to Time Slots
43 # Each resource can be assigned to work in different time slots
44 # This creates edges from each resource to each time slot
45 # Node numbering: resources are 1..resources, time slots are (resources+1)..(resources+time_slots)
46 for r in range(resources):
47 for t in range(time_slots):
48 start_nodes.append(r + 1) # From resource r
49 end_nodes.append(resources + 1 + t) # To time slot t
50 capacities.append(5) # Each resource can work 5 units per time slot (capacity constraint)
51 unit_costs.append(1) # Base cost for a resource working in a time slot
52
53 # Stage 3: Time Slots to Tasks
54 # Each time slot can contribute to completing different tasks
55 # This creates edges from each time slot to each task
56 # Node numbering: tasks are (resources+time_slots+1)..(resources+time_slots+tasks)
57 for t in range(time_slots):
58 for task in range(tasks):
59 start_nodes.append(resources + 1 + t) # From time slot t
60 end_nodes.append(resources + time_slots + 1 + task) # To task
61 capacities.append(3) # Each time slot can handle 3 units per task
62 unit_costs.append(t + 1) # Cost increases with later time slots (urgency penalty)
63 # This models that completing work earlier is preferred
64
65 # Stage 4: Tasks to Sink
66 # The sink node represents completed work
67 # All tasks must flow to the sink, representing that all tasks must be completed
68 sink = resources + time_slots + tasks + 1
69 for task in range(tasks):
70 start_nodes.append(resources + time_slots + 1 + task) # From task
71 end_nodes.append(sink) # To sink
72 capacities.append(8) # Each task can accept up to 8 units of work
73 unit_costs.append(0) # No cost for completing tasks (costs are in the process)
74
75 # Define node supplies/demands
76 # This enforces that:
77 # - Source supplies total capacity (20 units)
78 # - All intermediate nodes are transshipment (supply = 0)
79 # - Sink demands the same amount (20 units) to ensure all work is completed
80 supplies = [20] + [0] * (resources + time_slots + tasks) + [-20]
81
82 # Solve the optimization problem
83 # The solver will find the optimal way to route flow through all stages
84 # while minimizing total cost and respecting all capacity constraints
85 solution = solve_minimum_cost_flow(start_nodes, end_nodes, capacities, unit_costs, supplies)
86
87 # Analyze the solution by aggregating flows at each stage
88 # This helps us understand how the solver allocated resources
89 resource_usage = {} # How much each resource is used
90 time_slot_usage = {} # How much work is done in each time slot
91 task_usage = {} # How much work is allocated to each task
92
93 # Parse the solution to extract flow information
94 # We examine each edge in the solution and aggregate flows by stage
95 if solution['status'] == 'OPTIMAL':
96 for arc_info in solution['arc_info']:
97 arc = arc_info['arc']
98 flow = arc_info['flow'] # The optimal flow x_ij on this edge
99
100 if flow > 0: # Only consider edges that are actually used
101 start, end = arc
102
103 # Stage 1: Source to Resources
104 # Track how much flow is allocated to each resource
105 if start == 0: # Edge starts at source
106 resource_usage[end] = resource_usage.get(end, 0) + flow
107
108 # Stage 2: Resources to Time Slots
109 # Track how much work each resource does in each time slot
110 elif 1 <= start <= resources: # Edge starts at a resource
111 time_slot = end - resources - 1 # Extract time slot index
112 time_slot_usage[time_slot] = time_slot_usage.get(time_slot, 0) + flow
113
114 # Stage 3: Time Slots to Tasks
115 # Track how much work each time slot contributes to each task
116 elif resources + 1 <= start <= resources + time_slots: # Edge starts at a time slot
117 task = end - resources - time_slots - 1 # Extract task index
118 task_usage[task] = task_usage.get(task, 0) + flow
119
120 return solution, resource_usage, time_slot_usage, task_usage, resources, time_slots, tasks
121
122# Run the time slotting example
123time_slotting_solution, resource_usage, time_slot_usage, task_usage, num_resources, num_time_slots, num_tasks = time_slotting_example()Now let's display the results from the time slotting example:
Time Slotting Network: Resources: 2, Time Slots: 3, Tasks: 4 Total nodes: 11 Optimal solution found! Total cost: 50.0 (This represents the minimized cost of allocating resources across time slots and tasks) Optimal Resource Allocation: ======================================== Resource 1: 10 units allocated Resource 2: 10 units allocated Time Slot Utilization: ======================================== Time slot 0: 10 units of work Time slot 1: 10 units of work Task Work Allocation: ======================================== Task 0: 2 units of work allocated Task 1: 6 units of work allocated Task 2: 6 units of work allocated Task 3: 6 units of work allocated
The solver found an optimal allocation that routes resources through time slots to tasks while minimizing total cost. The solution balances multiple competing factors: resource capacity limits prevent over-allocation, time slot availability constrains when work can be done, and task requirements ensure all work is completed. Notice that the cost structure (increasing costs for later time slots) encourages the solver to prefer earlier time slots when possible, which aligns with operational preferences for completing work sooner rather than later. The total cost reflects the trade-offs between these constraints, and the allocation shows how resources are efficiently distributed across the network to achieve this optimal balance.
Alternative Implementation: Direct Solver Usage
Sometimes it's useful to work directly with the OR-Tools solver object rather than using a wrapper function. This gives you more control and helps you understand exactly how the solver API maps to our mathematical formulation. Let's see how to build a network step-by-step:
1def manual_network_construction():
2 """
3 Manually construct and solve a simple MCF problem using direct solver API.
4
5 This demonstrates the direct mapping between mathematical concepts and code:
6 - Each edge (i,j) → add_arc_with_capacity_and_unit_cost(i, j, u_ij, c_ij)
7 - Each node supply b_i → set_node_supply(i, b_i)
8 - Query results → flow(i), optimal_cost()
9
10 This approach is useful when you want fine-grained control over the problem
11 construction or when building networks incrementally.
12 """
13
14 # Initialize the solver
15 # This creates an empty network that we'll build edge by edge
16 solver = min_cost_flow.SimpleMinCostFlow()
17
18 # Step 1: Add edges (arcs) to the network
19 # Each call adds one edge (i,j) with capacity u_ij and unit cost c_ij
20 # This directly corresponds to defining the set E in our graph G = (V, E)
21
22 solver.add_arc_with_capacity_and_unit_cost(0, 1, 10, 2) # Edge (0,1): u=10, c=2
23 solver.add_arc_with_capacity_and_unit_cost(0, 2, 8, 4) # Edge (0,2): u=8, c=4
24 solver.add_arc_with_capacity_and_unit_cost(1, 2, 5, 1) # Edge (1,2): u=5, c=1
25 solver.add_arc_with_capacity_and_unit_cost(1, 3, 12, 3) # Edge (1,3): u=12, c=3
26 solver.add_arc_with_capacity_and_unit_cost(2, 3, 15, 2) # Edge (2,3): u=15, c=2
27
28 # Step 2: Set node supplies/demands
29 # This sets the values b_i for each node, which the solver uses to enforce
30 # flow conservation: Σx_out - Σx_in = b_i for each node i
31
32 solver.set_node_supply(0, 15) # Node 0: supply node (b_0 = 15)
33 solver.set_node_supply(1, 0) # Node 1: transshipment (b_1 = 0)
34 solver.set_node_supply(2, 0) # Node 2: transshipment (b_2 = 0)
35 solver.set_node_supply(3, -15) # Node 3: demand node (b_3 = -15)
36
37 # Step 3: Solve the optimization problem
38 # The solver automatically:
39 # - Enforces flow conservation at each node
40 # - Enforces capacity constraints on each edge
41 # - Minimizes total cost: min Σ c_ij * x_ij
42 status = solver.solve()
43
44 # Extract solution information
45 solution_data = {
46 'status': status,
47 'optimal': status == solver.OPTIMAL,
48 'cost': solver.optimal_cost() if status == solver.OPTIMAL else None,
49 'arcs': []
50 }
51
52 if status == solver.OPTIMAL:
53 # Extract the solution: the optimal values of our decision variables x_ij
54 # The solver stores arcs in the order they were added
55 for i in range(solver.num_arcs()):
56 tail = solver.tail(i) # Start node of arc i
57 head = solver.head(i) # End node of arc i
58 flow = solver.flow(i) # Optimal flow x_ij on this arc
59 capacity = solver.capacity(i) # Capacity u_ij
60 unit_cost = solver.unit_cost(i) # Unit cost c_ij
61 total_cost = flow * unit_cost # Cost contribution: c_ij * x_ij
62
63 solution_data['arcs'].append({
64 'tail': tail,
65 'head': head,
66 'flow': flow,
67 'capacity': capacity,
68 'unit_cost': unit_cost,
69 'total_cost': total_cost
70 })
71
72 return solution_data
73
74# Run manual example
75# This demonstrates the direct API usage, which is useful for understanding
76# how the mathematical formulation maps to code
77manual_solution = manual_network_construction()1def manual_network_construction():
2 """
3 Manually construct and solve a simple MCF problem using direct solver API.
4
5 This demonstrates the direct mapping between mathematical concepts and code:
6 - Each edge (i,j) → add_arc_with_capacity_and_unit_cost(i, j, u_ij, c_ij)
7 - Each node supply b_i → set_node_supply(i, b_i)
8 - Query results → flow(i), optimal_cost()
9
10 This approach is useful when you want fine-grained control over the problem
11 construction or when building networks incrementally.
12 """
13
14 # Initialize the solver
15 # This creates an empty network that we'll build edge by edge
16 solver = min_cost_flow.SimpleMinCostFlow()
17
18 # Step 1: Add edges (arcs) to the network
19 # Each call adds one edge (i,j) with capacity u_ij and unit cost c_ij
20 # This directly corresponds to defining the set E in our graph G = (V, E)
21
22 solver.add_arc_with_capacity_and_unit_cost(0, 1, 10, 2) # Edge (0,1): u=10, c=2
23 solver.add_arc_with_capacity_and_unit_cost(0, 2, 8, 4) # Edge (0,2): u=8, c=4
24 solver.add_arc_with_capacity_and_unit_cost(1, 2, 5, 1) # Edge (1,2): u=5, c=1
25 solver.add_arc_with_capacity_and_unit_cost(1, 3, 12, 3) # Edge (1,3): u=12, c=3
26 solver.add_arc_with_capacity_and_unit_cost(2, 3, 15, 2) # Edge (2,3): u=15, c=2
27
28 # Step 2: Set node supplies/demands
29 # This sets the values b_i for each node, which the solver uses to enforce
30 # flow conservation: Σx_out - Σx_in = b_i for each node i
31
32 solver.set_node_supply(0, 15) # Node 0: supply node (b_0 = 15)
33 solver.set_node_supply(1, 0) # Node 1: transshipment (b_1 = 0)
34 solver.set_node_supply(2, 0) # Node 2: transshipment (b_2 = 0)
35 solver.set_node_supply(3, -15) # Node 3: demand node (b_3 = -15)
36
37 # Step 3: Solve the optimization problem
38 # The solver automatically:
39 # - Enforces flow conservation at each node
40 # - Enforces capacity constraints on each edge
41 # - Minimizes total cost: min Σ c_ij * x_ij
42 status = solver.solve()
43
44 # Extract solution information
45 solution_data = {
46 'status': status,
47 'optimal': status == solver.OPTIMAL,
48 'cost': solver.optimal_cost() if status == solver.OPTIMAL else None,
49 'arcs': []
50 }
51
52 if status == solver.OPTIMAL:
53 # Extract the solution: the optimal values of our decision variables x_ij
54 # The solver stores arcs in the order they were added
55 for i in range(solver.num_arcs()):
56 tail = solver.tail(i) # Start node of arc i
57 head = solver.head(i) # End node of arc i
58 flow = solver.flow(i) # Optimal flow x_ij on this arc
59 capacity = solver.capacity(i) # Capacity u_ij
60 unit_cost = solver.unit_cost(i) # Unit cost c_ij
61 total_cost = flow * unit_cost # Cost contribution: c_ij * x_ij
62
63 solution_data['arcs'].append({
64 'tail': tail,
65 'head': head,
66 'flow': flow,
67 'capacity': capacity,
68 'unit_cost': unit_cost,
69 'total_cost': total_cost
70 })
71
72 return solution_data
73
74# Run manual example
75# This demonstrates the direct API usage, which is useful for understanding
76# how the mathematical formulation maps to code
77manual_solution = manual_network_construction()Now let's display the results from the manual network construction:
Manual Network Construction: ================================================== Optimal total cost: 80.0 (This is the minimized value of Σ c_ij * x_ij) Optimal flow on each arc: Arc Flow Capacity Unit Cost Total Cost -------------------------------------------------- 0 -> 1: 10 10 2.0 20.0 0 -> 2: 5 8 4.0 20.0 1 -> 2: 5 5 1.0 5.0 1 -> 3: 5 12 3.0 15.0 2 -> 3: 10 15 2.0 20.0 Key insight: The solver found the optimal flow values x_ij that minimize total cost while satisfying all flow conservation and capacity constraints. Notice that some edges may have zero flow if they're not part of the optimal solution.
The solver successfully found an optimal solution for this network. The total cost represents the minimum achievable cost given the network structure, capacities, and unit costs. Each arc's flow value shows how the solver allocated flow to minimize total cost while respecting capacity constraints. Arcs with zero flow indicate that those edges are not part of the optimal solution. The solver determined that routing flow through alternative paths is more cost-effective. This demonstrates how the MCF solver automatically identifies the most efficient routing strategy without requiring manual path selection.
Key Parameters
Below are the main parameters and methods that affect how Minimum Cost Flow problems are solved using OR-Tools.
Key Parameters
The SimpleMinCostFlow solver in OR-Tools doesn't expose many configurable parameters directly, as it uses specialized algorithms optimized for network flow problems. However, the problem structure itself is defined by several key parameters:
-
start_nodes: List of starting nodes for each arc (the "i" in edge ). Each element corresponds to one edge in the network. Should have the same length asend_nodes,capacities, andunit_costs. -
end_nodes: List of ending nodes for each arc (the "j" in edge ). Together withstart_nodes, this defines the network topology. Node indices should be non-negative integers starting from 0. -
capacities: List of capacity values for each arc (maximum flow allowed). Should be non-negative. Use realistic values based on actual operational constraints rather than arbitrarily large numbers. Values should be in the same units as flow variables. -
unit_costs: List of unit cost values for each arc (cost per unit of flow). Can be any real number (positive, negative, or zero). Negative costs can model revenue or benefits. Ensure all costs use consistent units (cost per flow unit). -
supplies: List of supply values for each node. Positive values represent sources (supply nodes), negative values represent sinks (demand nodes), and zero represents transshipment nodes. The sum of all positive supplies should equal the absolute sum of all negative demands for the problem to be feasible.
Key Methods
The following are the most commonly used methods for working with the SimpleMinCostFlow solver:
-
add_arc_with_capacity_and_unit_cost(start_node, end_node, capacity, unit_cost): Adds an edge to the network with specified capacity and unit cost. Call this method once for each edge in your network. The order of addition doesn't affect the solution, but it determines the arc index used for querying results. -
set_node_supply(node, supply): Sets the supply value for a node. Call this method once for each node in your network. Node indices should be consecutive starting from 0. Positive values indicate supply, negative values indicate demand, and zero indicates transshipment. -
solve(): Solves the optimization problem and returns a status code. Returnssolver.OPTIMALif an optimal solution was found,solver.INFEASIBLEif no feasible solution exists, orsolver.UNKNOWNif the solver encountered issues. Check the status before querying solution values. -
optimal_cost(): Returns the minimized total cost (the value of ) after solving. Only valid ifsolve()returnedOPTIMAL. This represents the minimum achievable cost for routing flow through the network. -
flow(arc_index): Returns the optimal flow value for the arc at the given index. Arc indices correspond to the order in which arcs were added usingadd_arc_with_capacity_and_unit_cost(). Only valid after a successful solve. -
num_arcs(): Returns the total number of arcs (edges) in the network. Useful for iterating over all arcs to extract solution information. -
tail(arc_index)andhead(arc_index): Return the start and end nodes of an arc, respectively. Useful for identifying which edge each flow value corresponds to when extracting solutions.
Practical Applications
Practical Implications
The Minimum Cost flow approach is particularly valuable in several practical scenarios where optimal resource allocation is critical. In supply chain management, MCF helps optimize the flow of goods from suppliers through distribution centers to customers, taking into account transportation costs, warehouse capacities, and demand patterns. This is especially important for companies with complex distribution networks where multiple paths exist between sources and destinations. The network structure naturally captures the flow of products through various stages, making it intuitive to model real-world logistics challenges.
In workforce scheduling, MCF can be used to assign employees to shifts while minimizing costs and respecting availability constraints. The network structure naturally models the flow of labor resources through different time periods and job assignments. This approach is particularly effective when there are complex constraints involving skill requirements, labor regulations, and operational needs. The ability to handle multiple time periods and resource types simultaneously makes MCF well-suited for scheduling problems that would be difficult to model with simpler optimization approaches.
For project scheduling and resource allocation, MCF provides a framework for optimally allocating resources across different project phases while minimizing costs. The network can represent the flow of resources (personnel, equipment, materials) through different project stages, with costs reflecting the efficiency or preference for different resource-time combinations. MCF is most appropriate when your problem has a clear network structure with sources (resource availability), sinks (demand points), and intermediate nodes (processing stages or time periods). Avoid MCF when your problem requires non-linear cost functions, complex precedence relationships that don't map naturally to network flows, or when indivisibility constraints prevent flow splitting at nodes.
Best Practices
To achieve optimal results with Minimum Cost flow problems, follow several best practices. First, ensure that your network structure accurately represents the problem domain. Each node should have a clear role as either a source (supply), sink (demand), or transshipment point. When defining capacities, use realistic values based on actual operational constraints rather than arbitrary large numbers, as overly generous capacities can lead to solutions that are mathematically optimal but operationally infeasible. For cost parameters, use consistent units across all edges and ensure that costs reflect the true economic or operational impact of routing flow through each pathway.
When working with OR-Tools, use the SimpleMinCostFlow solver for standard MCF problems, as it provides efficient specialized algorithms. For problems where you need to verify feasibility before optimization, check that the total supply equals the total demand (accounting for signs: positive supplies and negative demands should sum to zero). If your problem has integer supplies, demands, and capacities, you can rely on the integrality property to guarantee integer solutions without explicitly adding integer constraints. For reproducibility, set random seeds if using any randomized preprocessing steps, though the MCF solver itself is deterministic.
When evaluating solutions, verify that all flow conservation constraints are satisfied and that no capacity constraints are violated. The solver will report if the problem is infeasible, but you should also validate that your input data makes physical sense. If you encounter infeasibility, systematically check whether total supply matches total demand, whether capacity constraints are too restrictive, and whether the network structure allows flow to reach all demand nodes from supply nodes. For large problems, consider breaking them into smaller subproblems or using hierarchical approaches where MCF solves subproblems within a larger optimization framework.
Data Requirements and Preprocessing
The MCF approach requires accurate data on network structure, capacities, costs, and supply/demand patterns. The quality of the solution depends heavily on the accuracy of these input parameters. Network structure data should specify all nodes and edges, including their connectivity relationships. For each edge, you need both capacity limits (the maximum flow that can pass through) and unit costs (the cost per unit of flow). These values should be in consistent units: capacities and flows in the same units (e.g., units, hours, workers), and costs in cost units per flow unit.
Before constructing your network, preprocess your data to handle missing values, outliers, and data quality issues. If capacity data is uncertain, consider using conservative estimates or running sensitivity analysis to understand how capacity changes affect the solution. Cost data should reflect the true economic or operational costs, including transportation expenses, time penalties, resource utilization costs, and any preference-based costs. If costs vary with flow volume (non-linear), you may need to approximate with piecewise linear costs or consider alternative formulations.
For supply and demand values, ensure that positive supplies (sources) and negative demands (sinks) are correctly specified. The sum of all supplies should equal the absolute sum of all demands for the problem to be feasible. If your problem has time-based components, structure your network with time periods as separate nodes or layers, ensuring that flow can only move forward in time. When dealing with resource allocation problems, map resources, time slots, and tasks to appropriate network layers, with capacities representing resource availability and task requirements.
Common Pitfalls
Several common pitfalls can undermine the effectiveness of Minimum Cost flow solutions if not carefully addressed. One frequent mistake is mismatched supply and demand totals, where the sum of positive supplies does not equal the absolute sum of negative demands. This will cause the solver to report infeasibility even when a solution might seem possible. Verify that before solving. Another issue arises when capacity constraints are set too restrictively, preventing flow from reaching demand nodes even when sufficient supply exists. Review your capacity values to ensure they allow feasible flow paths through the network.
Neglecting to account for all relevant costs can lead to solutions that are mathematically optimal but operationally suboptimal. For example, if you only include transportation costs but ignore time penalties or resource utilization costs, the solution may route flow through paths that are cheap in terms of transportation but expensive in other ways. Include all significant cost components in your edge cost parameters. Similarly, using inconsistent units for capacities and costs can produce misleading results. Ensure that all capacity values use the same units as flow variables, and that all cost values represent cost per unit of flow in consistent currency or utility units.
Another common mistake is creating network structures that don't accurately represent the problem constraints. For instance, if your problem requires that certain resources cannot be split (indivisibility), the standard MCF formulation may not be appropriate since it allows flow to be split and combined freely at nodes. In such cases, you may need to use integer programming extensions or reformulate the problem. Finally, failing to validate solutions can lead to implementing infeasible or suboptimal allocations. Check that the solution satisfies flow conservation at all nodes, respects capacity constraints on all edges, and makes operational sense in your specific context.
Computational Considerations
MCF problems are computationally efficient compared to general linear programming problems due to their special network structure. The OR-Tools SimpleMinCostFlow solver uses specialized algorithms (such as cost scaling or successive shortest path) that typically solve problems with thousands of nodes and edges in seconds or milliseconds. For problems with up to approximately 10,000 nodes and 100,000 edges, the solver usually performs well on modern hardware. Beyond this scale, you may need to consider problem decomposition, network simplification, or approximate methods.
Memory requirements are generally modest for MCF problems, as the solver only needs to store the network structure (nodes, edges, capacities, costs) and the solution (flow values). For very large problems (millions of edges), consider using sparse data structures and avoid storing full dense matrices. If memory becomes a constraint, you can process the network in chunks or use iterative refinement approaches where you solve subproblems and combine solutions.
For problems that need to be solved repeatedly (such as real-time scheduling or dynamic resource allocation), the MCF solver's speed makes it suitable for online applications. However, if your problem structure changes frequently, you may need to rebuild the network each time, which adds overhead. In such cases, consider maintaining a base network structure and updating only the changed parameters (capacities, costs, or supplies) rather than reconstructing the entire network. For problems with similar structure but different parameter values, you can often reuse network construction code and simply update the parameter arrays.
Performance and Deployment Considerations
Evaluating MCF solution quality involves checking both mathematical optimality and operational feasibility. The solver reports whether an optimal solution was found, but you should also verify that the solution makes sense in your specific context. Key metrics to consider include total cost (the minimized objective value), flow utilization (what percentage of available capacity is used), and solution balance (how evenly flow is distributed across available paths). For operational validation, compare the solution against known good solutions or historical allocations to ensure it aligns with domain expectations.
When deploying MCF solutions in production, consider how frequently the problem needs to be solved and how quickly solutions are required. The solver's speed makes it suitable for real-time applications, but you should account for data preparation time, network construction overhead, and solution interpretation. For batch processing applications, you can solve multiple problem instances in parallel if they are independent. If problems are related (e.g., daily scheduling where today's solution affects tomorrow's starting state), you may need to solve them sequentially and update initial conditions between solves.
Monitoring deployed MCF solutions involves tracking both solution quality and solver performance. If solution costs increase unexpectedly, investigate whether input parameters (costs, capacities, demands) have changed or whether the problem structure no longer accurately represents the operational reality. If solver times increase significantly, check whether the problem size has grown or whether the network structure has become more complex. For critical applications, implement validation checks that flag solutions with unusual characteristics (e.g., very high or very low capacity utilization, unexpected flow patterns) for human review before implementation.
Summary
The Minimum Cost flow problem provides a powerful and efficient framework for solving resource allocation and slotting problems. By modeling problems as network flows with sources, sinks, and capacity constraints, we can leverage specialized algorithms that guarantee optimal solutions while remaining computationally tractable for many real-world applications.
The mathematical foundation of MCF, based on flow conservation and capacity constraints, provides both theoretical rigor and practical flexibility. The network representation makes it intuitive to understand and visualize, while the linear programming structure ensures efficient solution algorithms. The integrality property of MCF problems is particularly valuable for applications where fractional allocations don't make sense.
For practitioners, OR-Tools provides an excellent implementation of MCF algorithms that can handle complex networks with thousands of nodes and edges. The framework's flexibility allows for easy incorporation of various constraints and cost structures, making it suitable for a wide range of applications from supply chain optimization to workforce scheduling. When your problem naturally fits a network flow structure, MCF often provides the most efficient and effective solution approach.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about minimum cost flow optimization and network flow problems.
Reference

About the author: Michael Brenndoerfer
All opinions expressed here are my own and do not reflect the views of my employer.
Michael currently works as an Associate Director of Data Science at EQT Partners in Singapore, where he drives AI and data initiatives across private capital investments.
With over a decade of experience spanning private equity, management consulting, and software engineering, he specializes in building and scaling analytics capabilities from the ground up. He has published research in leading AI conferences and holds expertise in machine learning, natural language processing, and value creation through data.
Related Content

Mixed Integer Linear Programming (MILP) for Factory Optimization: Complete Guide with Mathematical Foundations & Implementation
Complete guide to Mixed Integer Linear Programming (MILP) for factory optimization, covering mathematical foundations, constraint modeling, branch-and-bound algorithms, and practical implementation with Google OR-Tools. Learn how to optimize production planning with discrete setup decisions and continuous quantities.

CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling
Learn CP-SAT rostering using Google OR-Tools to solve complex workforce scheduling problems with binary decision variables, coverage constraints, and employee availability. Master constraint programming for optimal employee shift assignments.

NHITS: Neural Hierarchical Interpolation for Time Series Forecasting with Multi-Scale Decomposition & Implementation
Master NHITS (Neural Hierarchical Interpolation for Time Series), a deep learning architecture for multi-scale time series forecasting. Learn hierarchical decomposition, neural interpolation, and how to implement NHITS for complex temporal patterns in retail, energy, and financial data.
Stay updated
Get notified when I publish new articles on data and AI, private equity, technology, and more.
