Capacitated VRP (CVRP)
What’s added over plain VRP
Two new things:
- Each customer
ihas a demandq_i(boxes, weight, volume, one scalar per node). - Each vehicle has a capacity
Q. The total demand on any single route cannot exceedQ.
Everything else, objective, sequencing, depot, is unchanged from plain VRP. Most real-world delivery problems are at minimum CVRP; skipping capacity is rare outside textbook TSP.
The picture
A CVRP solution partitions customers such that no route exceeds the vehicle capacity. In the canonical OR-Tools example (16 customers, 4 vehicles, Q = 15 per vehicle), routes end up roughly balanced in total demand, not necessarily balanced in number of stops.
Capacity Q = 15 per vehicle.
Route A (load 14): DEPOT → C2(4) → C5(2) → C9(8) → DEPOTRoute B (load 13): DEPOT → C1(3) → C7(6) → C11(4) → DEPOTRoute C (load 15): DEPOT → C3(5) → C8(4) → C13(6) → DEPOTRoute D (load 12): DEPOT → C4(7) → C6(5) → DEPOT
Unassigned: (none, every customer is visited) Objective: minimize total distance across all 4 routesFormulation
Given:
ncustomers plus 1 depot (index 0), totaln+1nodes- Demand
q_i ≥ 0fori = 1..n,q_0 = 0 - Cost
c_{ij}between every pair Kvehicles, capacityQ
Decision variable x_{ij}^k = 1 iff vehicle k traverses edge (i, j).
Minimize Σ_k Σ_{i,j} c_{ij} · x_{ij}^k
Subject to:
- Every customer visited once, inflow and outflow each equal 1 per customer across all vehicles
- Flow conservation at every node, inflow equals outflow per vehicle
- Capacity,
Σ_i (Σ_j x_{ij}^k) · q_i ≤ Qfor every vehiclek - Subtour elimination (polynomially many constraints via MTZ or flow formulation)
OR-Tools sketch
In OR-Tools, you model the capacity constraint as a dimension:
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
# 1. Manager translates between node indices and solver variable indicesmanager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot=0)
# 2. Routing model, the solver objectrouting = pywrapcp.RoutingModel(manager)
# 3. Transit callback, cost of traveling from one node to anotherdef distance_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) return distance_matrix[from_node][to_node]
transit_idx = routing.RegisterTransitCallback(distance_callback)routing.SetArcCostEvaluatorOfAllVehicles(transit_idx)
# 4. Demand callback + capacity dimension, this is the CVRP-specific partdef demand_callback(from_index): from_node = manager.IndexToNode(from_index) return demands[from_node]
demand_idx = routing.RegisterUnaryTransitCallback(demand_callback)routing.AddDimensionWithVehicleCapacity( demand_idx, slack_max=0, # no overflow allowed vehicle_capacities=vehicle_caps, # list: one capacity per vehicle fix_start_cumul_to_zero=True, name="Capacity",)
# 5. Search parametersparams = pywrapcp.DefaultRoutingSearchParameters()params.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARCparams.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCHparams.time_limit.FromSeconds(30)
# 6. Solvesolution = routing.SolveWithParameters(params)The pattern, register a callback, add a dimension, constrain the dimension, extends directly to VRPTW (dimension = time) and other variants.
Common gotchas
- Integer demands required. OR-Tools expects integer returns from
demand_callback. Scale decimal demands (multiply by 10, 100) if needed. slack_max = 0on capacity. Non-zero slack would let some capacity spill over, almost never what you want.- Heterogeneous fleet. Pass a list of per-vehicle capacities to
AddDimensionWithVehicleCapacity; don’t try a single value. - Infeasibility on tight fleets. If total demand exceeds
K × Q, the problem has no feasible solution. Either add vehicles, allow dropped visits (with penalties), or raise capacity. - Multiple depot variant (MDVRP). Construct the
RoutingIndexManagerwith per-vehicle start/end lists instead of a singledepotarg.
References
- Capacity Constraints, OR-Tools
- RoutingModel reference, OR-Tools
- CVRPLIB benchmark sets, X-class is the current standard