Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools
Back to Writing

Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools

Michael BrenndoerferNovember 27, 202552 min read12,386 wordsInteractive

Master the Vehicle Routing Problem with Time Windows (VRPTW), including mathematical formulation, constraint programming, and practical implementation using Google OR-Tools for logistics optimization.

Data Science Handbook Cover
Part of Data Science Handbook

This article is part of the free-to-read Data Science Handbook

Reading Level

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.

Vehicle Routing Problem with Time Windows (VRPTW)Link Copied

The Vehicle Routing Problem with Time Windows (VRPTW) is a fundamental optimization challenge in logistics and supply chain management. At its core, VRPTW seeks to determine the most efficient routes for a fleet of vehicles to serve a set of customers, where each customer must be visited within a specific time window. This problem extends the classic Vehicle Routing Problem (VRP) by incorporating temporal constraints that reflect real-world delivery scenarios where customers have preferred or required service times.

The VRPTW is particularly relevant in modern logistics operations, from e-commerce delivery to field service management. Unlike simple routing problems that only consider distance or travel time, VRPTW must balance multiple competing objectives: minimizing total travel cost, respecting vehicle capacity constraints, and ensuring all customers are served within their specified time windows. This makes it a complex combinatorial optimization problem that often requires sophisticated algorithms to solve efficiently.

The problem becomes exponentially more complex as the number of customers and vehicles increases, making it computationally challenging for large-scale instances. However, modern optimization tools like Google OR-Tools provide powerful solvers that can handle realistic problem sizes while finding high-quality solutions in reasonable time.

AdvantagesLink Copied

VRPTW solutions can significantly reduce total travel time and distance, leading to lower fuel costs, reduced vehicle wear, and improved driver productivity. By optimizing routes, companies can serve more customers with the same fleet size or maintain service levels with fewer vehicles. These operational efficiencies translate directly to bottom-line savings, making VRPTW optimization a high-value investment for logistics operations of any scale.

Time window constraints ensure that deliveries arrive when customers expect them, improving service quality and customer satisfaction. This is particularly important for time-sensitive deliveries like medical supplies, perishable goods, or scheduled appointments where late arrivals can have serious consequences. By explicitly modeling customer preferences, VRPTW solutions help businesses build trust and maintain strong customer relationships.

Modern VRPTW solvers can handle problems with hundreds of customers and dozens of vehicles, making them suitable for real-world applications. The solutions can be updated dynamically as new orders arrive or conditions change, allowing dispatchers to respond to evolving circumstances throughout the day. This scalability and adaptability make VRPTW a practical tool for both strategic route planning and day-to-day operational decisions.

DisadvantagesLink Copied

As a variant of the Traveling Salesman Problem, VRPTW is NP-hard, meaning solution time grows exponentially with problem size. While modern solvers can handle realistic instances with hundreds of customers, very large problems may require heuristic approaches or problem decomposition strategies that sacrifice guaranteed optimality for computational tractability.

Finding optimal solutions for large instances can be computationally expensive, forcing practitioners to balance solution quality with computation time. In practice, this often means accepting near-optimal solutions that can be found quickly rather than waiting hours or days for provably optimal routes. The gap between the best-known solution and the theoretical optimum may be acceptable for operational purposes but can be frustrating when marginal improvements matter.

Real-world routing often involves dynamic changes such as new orders arriving throughout the day, unexpected traffic conditions, or vehicle breakdowns that can invalidate pre-computed solutions. This volatility requires either frequent re-optimization, which adds computational overhead, or robust solution approaches that build in slack to accommodate changes. Neither approach is perfect, and dispatchers must often make real-time adjustments that deviate from the optimized plan.

FormulaLink Copied

Every optimization problem tells a story of choices and consequences. In VRPTW, the story begins with a simple question: How do we teach a computer to make the same decisions a skilled dispatcher makes instinctively? A human dispatcher juggles multiple concerns simultaneously: which truck goes where, what time to arrive, how much each vehicle can carry. To solve this problem computationally, we must translate this intuition into precise mathematical language.

This section develops the mathematical formulation step by step. For each component, we'll ask: Why do we need this? and What happens if we leave it out? By the end, you'll understand both what the formulas say and why each component is important for capturing the real-world complexity of vehicle routing.

The Dispatcher's Dilemma: Three Intertwined DecisionsLink Copied

Picture yourself as a logistics dispatcher on a Monday morning. You have a fleet of delivery trucks, a list of customers expecting packages, and a day's worth of time windows to honor. Your job is to answer three fundamental questions:

  1. Assignment: Which vehicle should visit which customers?
  2. Sequencing: In what order should each vehicle make its stops?
  3. Timing: When exactly should each vehicle arrive at each location?

At first glance, these might seem like separate problems. But consider what happens when you try to solve them independently:

  • You assign Customer A to Truck 1 because it's nearby, but Truck 1 is already overloaded.
  • You sequence Customer B before Customer C because B is closer to the depot, but Customer B's time window doesn't open until the afternoon.
  • You schedule an arrival at 10:00 AM, but given travel times, the truck can't possibly get there before noon.

The fundamental insight is that these three decisions are deeply interconnected. The sequence of visits determines travel times, which determine arrival times, which must fit within time windows, which constrain which sequences are feasible, which affects how we assign customers to vehicles. It's a web of dependencies that we must capture mathematically.

The Language of Optimization: Parameters and VariablesLink Copied

Before we can express our dispatcher's decisions mathematically, we need to establish a precise vocabulary. In optimization, we distinguish between two types of quantities:

  • Parameters: Fixed information about the problem that we cannot change
  • Decision variables: The unknowns that the solver must determine

Think of parameters as the rules of the game, and decision variables as the moves we're trying to optimize.

Notation Reference

Throughout this formulation, we use the following notation:

SymbolTypeMeaning
V={0,1,...,n}V = \{0, 1, ..., n\}SetAll locations (0 = depot, others = customers)
K={1,2,...,m}K = \{1, 2, ..., m\}SetAvailable vehicles
A={(i,j):i,jV,ij}A = \{(i,j) : i,j \in V, i \neq j\}SetPossible direct connections (arcs)
did_iParameterDemand at customer ii
QkQ_kParameterCapacity of vehicle kk
tijt_{ij}ParameterTravel time from location ii to jj
[ai,bi][a_i, b_i]ParameterTime window at location ii
sis_iParameterService time at location ii
cijc_{ij}ParameterCost of traveling from ii to jj
xijkx_{ijk}Decision variable1 if vehicle kk travels from ii to jj, 0 otherwise
wikw_{ik}Decision variableArrival time of vehicle kk at location ii
MMConstantLarge value for Big-M constraints

The Problem Landscape (Parameters):

The world we're optimizing within is defined by several fixed quantities:

  • Locations (VV): The depot (node 0) serves as home base, where all vehicles start and must return. The remaining nodes {1,2,...,n}\{1, 2, ..., n\} represent customers awaiting service.

  • Fleet (KK): We have mm vehicles at our disposal. Each vehicle kk has a capacity QkQ_k representing the maximum total demand it can carry. A delivery van might hold 20 packages; a truck might have a 10-ton weight limit.

  • Network (AA): Between any two locations, there exists a potential route. The arc (i,j)(i,j) represents the possibility of traveling directly from ii to jj, with travel time tijt_{ij} and cost cijc_{ij}.

  • Customer Requirements: Each customer ii has a demand did_i (how much they need), a time window [ai,bi][a_i, b_i] (when they can receive service), and a service time sis_i (how long the delivery takes once the vehicle arrives).

The Decisions We Must Make (Variables):

Our mathematical model must determine two types of quantities:

  • Route structure (xijkx_{ijk}): For every possible arc (i,j)(i,j) and every vehicle kk, we must decide: does vehicle kk use this arc? The binary nature of this variable (it can only be 0 or 1) reflects the discrete reality of routing: a truck either takes a road or it doesn't.

  • Arrival schedule (wikw_{ik}): For every location and vehicle, we must determine the arrival time. Unlike routing decisions, timing is continuous: a vehicle might arrive at 10:15 or 10:16 or any time in between.

These two variable types encode the spatial dimension (where do vehicles go?) and the temporal dimension (when do they get there?) of our solution. The constraints we'll develop ensure these two dimensions remain consistent with each other and with real-world requirements.

The Objective: What Are We Trying to Achieve?Link Copied

With our vocabulary established, we can now state what we're trying to accomplish. In most routing problems, efficiency means minimizing total cost, whether that's travel time, distance, fuel consumption, or some combination.

minkK(i,j)Acijxijk\min \sum_{k \in K} \sum_{(i,j) \in A} c_{ij} x_{ijk}

Reading the formula: This expression says: "For every vehicle kk and every possible arc (i,j)(i,j), if that vehicle uses that arc (xijk=1x_{ijk} = 1), add the arc's cost cijc_{ij} to our total. If the arc isn't used (xijk=0x_{ijk} = 0), add nothing."

The binary variable xijkx_{ijk} acts as a switch: it "turns on" the cost cijc_{ij} only when the arc is actually part of a route. Summing over all vehicles and all arcs gives us the total operational cost of our solution.

What does "cost" mean in practice?

This formulation is flexible. The cost cijc_{ij} could represent:

  • Travel time (minimize total driving hours)
  • Distance (minimize fuel consumption)
  • Monetary cost (including tolls, fuel, driver wages)
  • Environmental impact (minimize carbon emissions)

The mathematical structure remains identical regardless of which metric we optimize.

The Constraints: Rules That Solutions Must ObeyLink Copied

The objective function tells us what we want to achieve, but without constraints, the solver would trivially "solve" the problem by not sending any vehicles anywhere: zero cost! Constraints encode the rules that separate valid solutions from invalid ones.

Each constraint we introduce addresses a specific way that a solution could fail in the real world. As we develop each constraint, we'll ask: What goes wrong if we omit this rule?

Constraint 1: Complete Service CoverageLink Copied

The requirement: Every customer must be visited exactly once by exactly one vehicle.

What could go wrong without it: A cost-minimizing solver might skip difficult-to-reach customers entirely, or send multiple vehicles to easy customers while ignoring others.

kKjV,jixijk=1iV{0}\sum_{k \in K} \sum_{j \in V, j \neq i} x_{ijk} = 1 \quad \forall i \in V \setminus \{0\}

where:

  • KK: set of vehicles {1,2,...,m}\{1, 2, ..., m\}
  • VV: set of all nodes (depot and customers)
  • xijkx_{ijk}: binary variable equal to 1 if vehicle kk travels from node ii to node jj
  • ii: customer node (the constraint applies to all customers, i.e., V{0}V \setminus \{0\})

Interpreting the mathematics: For each customer ii (excluding the depot), count how many arcs leave that customer across all vehicles and all possible destinations. That count must equal exactly 1.

The summation jV,jixijk\sum_{j \in V, j \neq i} x_{ijk} counts arcs of the form "ii → something" for vehicle kk, that is, arcs departing from customer ii. Summing over all vehicles kk gives the total number of departures from customer ii. Since every visit requires exactly one departure, requiring this sum to equal 1 ensures:

  • No customer is skipped (sum ≥ 1)
  • No customer is visited multiple times (sum ≤ 1)
Why count departures instead of arrivals?

For any valid route, a vehicle that enters a customer must also leave. Thus, counting departing arcs is equivalent to counting arriving arcs. The formulation could equivalently use kjixjik=1\sum_{k} \sum_{j \neq i} x_{jik} = 1 (counting arrivals), but the departure form is more common in the literature.

This constraint forms the foundation of our model. It defines what it means to "serve" all customers.

Constraint 2: Vehicle Capacity LimitsLink Copied

The requirement: No vehicle can carry more than its capacity allows.

What could go wrong without it: The solver might assign every customer to a single vehicle if that minimizes travel cost, ignoring physical impossibility.

iV{0}dijV,jixijkQkkK\sum_{i \in V \setminus \{0\}} d_i \sum_{j \in V, j \neq i} x_{ijk} \leq Q_k \quad \forall k \in K

where:

  • V{0}V \setminus \{0\}: set of customer nodes (excluding the depot)
  • did_i: demand at customer ii (units of capacity required)
  • xijkx_{ijk}: binary variable equal to 1 if vehicle kk travels from ii to jj
  • QkQ_k: capacity of vehicle kk (maximum total demand it can serve)
  • kk: vehicle index

Interpreting the mathematics: For each vehicle kk, we compute the total demand it serves. The inner sum jixijk\sum_{j \neq i} x_{ijk} equals 1 if vehicle kk visits customer ii, and 0 otherwise. Multiplying by demand did_i and summing over all customers gives the total load. This must not exceed the vehicle's capacity QkQ_k.

Step-by-step logic:

  1. Does vehicle kk visit customer ii? Check if jixijk=1\sum_{j \neq i} x_{ijk} = 1 (yes) or 0 (no)
  2. If yes, that customer's demand did_i counts toward the vehicle's load
  3. Sum all such demands: idi(visited?)\sum_i d_i \cdot (\text{visited?})
  4. Ensure total ≤ capacity: the inequality Qk\leq Q_k

This constraint bridges the abstract routing model with physical reality. Trucks have finite space, and our solution must respect this.

Constraint 3: Flow Conservation (Route Continuity)Link Copied

The requirement: If a vehicle enters a location, it must also leave that location.

What could go wrong without it: The solver might create "broken" routes where vehicles appear at customers without a connected path from the depot, or routes that form isolated loops disconnected from the depot.

jV,jixijk=jV,jixjikiV,kK\sum_{j \in V, j \neq i} x_{ijk} = \sum_{j \in V, j \neq i} x_{jik} \quad \forall i \in V, \forall k \in K

where:

  • xijkx_{ijk}: binary variable equal to 1 if vehicle kk travels from ii to jj (arc leaving ii)
  • xjikx_{jik}: binary variable equal to 1 if vehicle kk travels from jj to ii (arc entering ii)
  • The constraint applies to all nodes iVi \in V and all vehicles kKk \in K

Interpreting the mathematics: This is a classic "flow balance" constraint from network optimization. For any node ii and vehicle kk:

  • Left side: jixijk\sum_{j \neq i} x_{ijk} counts arcs leaving ii (from ii to anywhere)
  • Right side: jixjik\sum_{j \neq i} x_{jik} counts arcs entering ii (from anywhere to ii)

These must be equal. Since all variables are binary, this means: enter a node exactly once → leave exactly once. Never enter → never leave.

Why this works: Combined with the service coverage constraint, flow conservation ensures that every vehicle's route forms a connected path starting at the depot, visiting some customers, and returning to the depot. There are no gaps, no teleportation, no vehicles stuck at customers.

Constraint 4: Time Window ComplianceLink Copied

The requirement: Arrivals must fall within each customer's specified time window.

What could go wrong without it: The solver might schedule arrivals at 3 AM for customers who are only open 9-5, or create routes that are spatially efficient but temporally infeasible.

aiwikbiiV,kKa_i \leq w_{ik} \leq b_i \quad \forall i \in V, \forall k \in K

where:

  • aia_i: earliest acceptable arrival time at location ii (lower bound of time window)
  • bib_i: latest acceptable arrival time at location ii (upper bound of time window)
  • wikw_{ik}: continuous variable representing when vehicle kk arrives at location ii
  • The constraint applies to all nodes iVi \in V and all vehicles kKk \in K

Interpreting the mathematics: This is a simple bound constraint on the arrival time variable. For each location ii and vehicle kk, the arrival time wikw_{ik} must be:

  • Not earlier than the earliest acceptable time aia_i
  • Not later than the latest acceptable time bib_i

The "TW" in VRPTW: This constraint is what distinguishes VRPTW from basic vehicle routing. Time windows add a temporal dimension that interacts with spatial routing decisions. You can't just find the shortest path; you must find a path that's also timely.

Constraint 5: Temporal Precedence (The Big-M Technique)Link Copied

The requirement: If a vehicle travels from ii to jj, the arrival time at jj must be consistent with departing from ii.

What could go wrong without it: The solver might set arrival times arbitrarily, claiming a vehicle arrives at customer B before customer A even though the route visits A first. The route structure and the timing schedule would be disconnected from each other.

wik+si+tijwjkM(1xijk)(i,j)A,kKw_{ik} + s_i + t_{ij} - w_{jk} \leq M(1 - x_{ijk}) \quad \forall (i,j) \in A, \forall k \in K

where:

  • wikw_{ik}: arrival time of vehicle kk at node ii
  • sis_i: service time at node ii (time spent performing the delivery)
  • tijt_{ij}: travel time from node ii to node jj
  • wjkw_{jk}: arrival time of vehicle kk at node jj
  • xijkx_{ijk}: binary variable equal to 1 if vehicle kk travels directly from ii to jj
  • MM: a sufficiently large constant (the "big-M" value)
  • AA: set of all arcs (possible direct connections)

This is the most subtle constraint in our formulation. It must accomplish something tricky: enforce a timing relationship only when an arc is actually used. The Big-M technique achieves this through clever algebraic manipulation.

Case 1: When the arc IS used (xijk=1x_{ijk} = 1):

The right-hand side becomes M(11)=0M(1-1) = 0, so the constraint simplifies to:

wik+si+tijwjkw_{ik} + s_i + t_{ij} \leq w_{jk}

This says: arrival at jj must be at least arrival at ii + service time at ii + travel time from ii to jj. Time moves forward along the route.

Case 2: When the arc is NOT used (xijk=0x_{ijk} = 0):

The right-hand side becomes M(10)=MM(1-0) = M, where MM is a very large number. The constraint becomes:

wik+si+tijwjkMw_{ik} + s_i + t_{ij} - w_{jk} \leq M

Since MM is huge, this is essentially always satisfied. The arrival times at ii and jj are independent, as they should be, since there's no direct route connecting them.

Choosing MM: The value must be large enough that the constraint is non-binding when xijk=0x_{ijk} = 0, but not so large as to cause numerical instability. A safe choice is M=max(bi)min(ai)+max(tij)+max(si)M = \max(b_i) - \min(a_i) + \max(t_{ij}) + \max(s_i), which represents the maximum possible time span in the problem.

Out[2]:
Visualization
Timeline diagram showing temporal constraint when routing arc is used, with service completion and travel time sequencing.
When the arc is used (x=1), the Big-M constraint becomes binding, enforcing that arrival at j must occur after completing service at i plus travel time. The timeline shows the temporal sequence: arrive at i, complete service, travel to j.
Timeline diagram showing relaxed constraint when routing arc is not used, with independent arrival times.
When the arc is not used (x=0), the large constant M makes the constraint trivially satisfied. The arrival times at i and j become independent, and no temporal relationship is required between them.

The diagram above makes the Big-M mechanism visible. The key insight is that this single constraint accomplishes two things: it enforces physical causality when routes are used, while remaining "invisible" for routes that don't exist in the solution. This conditional behavior is what makes Big-M constraints effective for modeling conditional relationships in optimization problems.

Constraint 6: The Binary DomainLink Copied

The requirement: Routing decisions must be discrete: either a vehicle takes an arc, or it doesn't.

xijk{0,1}(i,j)A,kKx_{ijk} \in \{0,1\} \quad \forall (i,j) \in A, \forall k \in K

where:

  • xijkx_{ijk}: the routing decision variable for arc (i,j)(i,j) and vehicle kk
  • {0,1}\{0,1\}: the binary domain (variable can only take value 0 or 1)
  • AA: set of all arcs
  • KK: set of all vehicles

This might seem like a trivial "constraint," but it's actually what makes VRPTW computationally challenging. If we allowed xijkx_{ijk} to take any value between 0 and 1 (a "relaxation"), we could solve the problem much faster using continuous optimization techniques. A solution might say "send 0.3 of a vehicle from A to B," which is mathematically valid but operationally meaningless.

The binary requirement forces discrete decisions: a truck either takes a road or it doesn't. There's no "half a route." This discreteness is what transforms a tractable linear program into the NP-hard mixed-integer program that VRPTW is known to be.

Seeing the System: How Constraints InteractLink Copied

We've now developed six constraints, but they don't operate in isolation. They form an interconnected system where a change in one part ripples through the others. Understanding this interaction is key to understanding why VRPTW is both powerful and challenging.

The Two Decision Dimensions:

Our model has two types of decision variables that the solver must determine simultaneously:

VariableTypeDimensionRole
xijkx_{ijk}BinarySpatialDefines the route structure
wikw_{ik}ContinuousTemporalDefines when events happen

These two dimensions are coupled through the precedence constraint (Big-M): the route structure determines which temporal relationships must hold.

The Constraint Ecosystem:

Think of the constraints as forming an ecosystem where each one addresses a potential failure mode:

  1. Service coverage ensures no customer is forgotten
  2. Capacity limits prevent physical overloading
  3. Flow conservation ensures routes are connected paths
  4. Time windows honor customer requirements
  5. Temporal precedence links space and time
  6. Binary domain forces discrete decisions

The solver's task is to find values for all variables that satisfy every constraint simultaneously while minimizing total cost. This is like solving a puzzle where each piece constrains what other pieces can be, except this puzzle has an exponentially large number of possible configurations.

Out[3]:
Visualization
Diagram showing interconnected VRPTW constraints including routing decisions, timing variables, capacity limits, flow conservation, time windows, and temporal precedence.
The VRPTW constraint system architecture showing how routing and timing decision variables connect through various constraints. The precedence constraint (Big-M) serves as the central link connecting routing decisions to timing feasibility.

The diagram above visualizes this architecture. Notice how the routing variables (xijkx_{ijk}) flow downward into spatial constraints (coverage, capacity, flow), while timing variables (wikw_{ik}) connect to temporal constraints (time windows). The precedence constraint sits at the center, serving as the bridge that couples these two worlds. This coupling is what makes VRPTW fundamentally more challenging than solving routing and scheduling as separate problems. The two dimensions must be optimized jointly.

The Computational Challenge: Why VRPTW Is HardLink Copied

Having built our mathematical model, we should understand what we're asking a computer to do when we hand it this formulation.

The Curse of Combinatorics:

VRPTW inherits its difficulty from the Traveling Salesman Problem (TSP), which asks: "What's the shortest route visiting nn cities exactly once?" TSP is NP-hard, meaning no known algorithm can solve all instances quickly. VRPTW is strictly harder: it's TSP with additional constraints layered on top.

Consider the size of the search space for a modest problem with nn customers and mm vehicles:

DecisionPossibilitiesGrowth Rate
Assign customers to vehiclesPartition nn items into mm groupsExponential in nn
Sequence within each routeOrder customers on each vehicleFactorial per route
Time each arrivalChoose wikw_{ik} satisfying windowsContinuous but constrained

For 20 customers and 3 vehicles, there are roughly 3203.5×1093^{20} \approx 3.5 \times 10^9 possible customer-vehicle assignments, and for each assignment, we must consider many possible orderings. The total number of potential solutions dwarfs the number of atoms in the observable universe for even moderately sized problems.

Why We Need Sophisticated Algorithms:

Enumeration (checking every possible solution) is clearly infeasible. Instead, modern solvers employ clever strategies:

  • Branch-and-bound: Systematically explore the solution space while pruning branches that can't lead to optimal solutions
  • Constraint propagation: Use constraints to eliminate infeasible variable values early
  • Metaheuristics: Use local search, simulated annealing, or genetic algorithms to find good (if not optimal) solutions quickly
  • Hybrid approaches: Combine exact methods with heuristics for the best of both worlds

The mathematical formulation we've developed is the foundation upon which all these algorithms operate. It precisely defines what we're looking for. The algorithms provide strategies for finding it.

Visualizing VRPTWLink Copied

Let's create visualizations to understand the VRPTW problem structure and solution characteristics.

Out[5]:
Visualization
Spatial map showing depot and 15 customer locations for VRPTW problem instance.
VRPTW problem layout showing the depot (red square) and 15 customer locations (blue circles) distributed across the service area. Each customer has a demand that must be satisfied and a time window for service.
Horizontal bar chart showing time window constraints for each of the 15 customers.
Customer time windows showing the service availability periods for each customer. The horizontal bars represent the time range during which each customer can be served. Overlapping windows create flexibility in route sequencing.
Out[6]:
Visualization
Spatial routes map showing three different colored vehicle routes connecting depot to 15 customer locations, forming efficient delivery paths.
Sample VRPTW solution showing three vehicle routes serving 15 customers. Each color represents a different vehicle's route, starting and ending at the depot (black square). The routes are designed to minimize total travel distance while respecting capacity constraints and customer time windows.

ExampleLink Copied

Now let's see how the mathematical formulation works in practice by solving a small, concrete example. This hands-on walkthrough will help you understand how the constraints interact and how a valid solution satisfies all requirements simultaneously.

Problem Setup: A Simple Delivery ScenarioLink Copied

Imagine you run a small delivery service with:

  • 1 depot (your warehouse, location 0)
  • 4 customers (locations 1-4) needing deliveries
  • 2 vehicles, each with capacity 10 units

Each customer has specific requirements: a demand (how much they need), a time window (when they can receive delivery), and travel times between locations. Let's examine the data:

Out[7]:
Example VRPTW Problem:

Distance Matrix (travel times between locations):
       Depot  C1  C2  C3  C4
Depot      0   2   3   4   5
C1         2   0   2   3   4
C2         3   2   0   2   3
C3         4   3   2   0   2
C4         5   4   3   2   0

Time Windows [earliest, latest]:
       Earliest  Latest
Depot         0      10
C1            1       4
C2            2       5
C3            3       6
C4            4       7

Customer Demands: [3, 2, 4, 3]
Vehicle Capacities: [10, 10]

Key observations from the data:

  • Customer 1 has the earliest time window (1-4) and is closest to the depot (travel time 2)
  • Customer 4 has the latest time window (4-7) and is farthest from the depot (travel time 5)
  • Total demand is 3+2+4+3 = 12 units, which exceeds either vehicle's capacity (10), so we need both vehicles
  • The time windows overlap, creating flexibility in sequencing

Building a Solution: Step-by-Step Constraint CheckingLink Copied

Let's construct a feasible solution manually, checking each constraint as we go. This demonstrates how the mathematical formulation translates to practical routing decisions.

Step 1: Assigning Customers to VehiclesLink Copied

We need to partition the 4 customers between 2 vehicles. Let's try:

  • Vehicle 1: Customers 1 and 3 (demands: 3 + 4 = 7 units)
  • Vehicle 2: Customers 2 and 4 (demands: 2 + 3 = 5 units)

Capacity check (Constraint 2):

  • Vehicle 1: 7 ≤ 10
  • Vehicle 2: 5 ≤ 10

Both assignments satisfy capacity constraints. Now we need to determine the sequence of visits for each vehicle.

Step 2: Sequencing Visits for Vehicle 1Link Copied

Vehicle 1 must visit Customers 1 and 3. Let's consider the sequence: Depot → Customer 1 → Customer 3 → Depot

Time window check (Constraints 4 and 5):

Let's trace the route step by step, using our notation:

  • Start at depot: w0,1=0w_{0,1} = 0 (vehicle 1 starts at depot at time 0)
  • Travel to C1: t0,1=2t_{0,1} = 2 (travel time from depot to customer 1)
    • Arrival at C1: w1,1=w0,1+t0,1=0+2=2w_{1,1} = w_{0,1} + t_{0,1} = 0 + 2 = 2
    • Time window check: a1=1w1,1=2b1=4a_1 = 1 \leq w_{1,1} = 2 \leq b_1 = 4 (Constraint 4 satisfied)
    • Service at C1: s1=0s_1 = 0 (assume 0 service time for simplicity)
  • Travel to C3: t1,3=3t_{1,3} = 3 (travel time from customer 1 to customer 3)
    • Arrival at C3: w3,1=w1,1+s1+t1,3=2+0+3=5w_{3,1} = w_{1,1} + s_1 + t_{1,3} = 2 + 0 + 3 = 5
    • Time window check: a3=3w3,1=5b3=6a_3 = 3 \leq w_{3,1} = 5 \leq b_3 = 6 (Constraint 4 satisfied)
    • Precedence check: w1,1+s1+t1,3=2+0+3=5w3,1=5w_{1,1} + s_1 + t_{1,3} = 2 + 0 + 3 = 5 \leq w_{3,1} = 5 (Constraint 5 satisfied)
  • Return to depot: t3,0=4t_{3,0} = 4 (travel time from customer 3 to depot)
    • Arrival at depot: w0,1=w3,1+s3+t3,0=5+0+4=9w_{0,1} = w_{3,1} + s_3 + t_{3,0} = 5 + 0 + 4 = 9

Flow conservation check (Constraint 3):

  • Vehicle 1 enters C1: 1 arc (from depot)
  • Vehicle 1 leaves C1: 1 arc (to C3)
  • Vehicle 1 enters C3: 1 arc (from C1)
  • Vehicle 1 leaves C3: 1 arc (to depot)

Route 1 is feasible! In terms of our decision variables:

  • x0,1,1=1x_{0,1,1} = 1 (vehicle 1 travels depot→C1)
  • x1,3,1=1x_{1,3,1} = 1 (vehicle 1 travels C1→C3)
  • x3,0,1=1x_{3,0,1} = 1 (vehicle 1 travels C3→depot)
  • w1,1=2w_{1,1} = 2, w3,1=5w_{3,1} = 5 (arrival times)

Step 3: Sequencing Visits for Vehicle 2Link Copied

Vehicle 2 must visit Customers 2 and 4. Let's try: Depot → Customer 2 → Customer 4 → Depot

Time window check:

Tracing route 2 with our notation:

  • Start at depot: w0,2=0w_{0,2} = 0 (vehicle 2 starts at depot at time 0)
  • Travel to C2: t0,2=3t_{0,2} = 3 (travel time from depot to customer 2)
    • Arrival at C2: w2,2=w0,2+t0,2=0+3=3w_{2,2} = w_{0,2} + t_{0,2} = 0 + 3 = 3
    • Time window check: a2=2w2,2=3b2=5a_2 = 2 \leq w_{2,2} = 3 \leq b_2 = 5 (Constraint 4 satisfied)
    • Service at C2: s2=0s_2 = 0 (assume 0 service time)
  • Travel to C4: t2,4=3t_{2,4} = 3 (travel time from customer 2 to customer 4, from distance matrix row 2, column 4)
    • Arrival at C4: w4,2=w2,2+s2+t2,4=3+0+3=6w_{4,2} = w_{2,2} + s_2 + t_{2,4} = 3 + 0 + 3 = 6
    • Time window check: a4=4w4,2=6b4=7a_4 = 4 \leq w_{4,2} = 6 \leq b_4 = 7 (Constraint 4 satisfied)
    • Precedence check: w2,2+s2+t2,4=3+0+3=6w4,2=6w_{2,2} + s_2 + t_{2,4} = 3 + 0 + 3 = 6 \leq w_{4,2} = 6 (Constraint 5 satisfied)
  • Return to depot: t4,0=5t_{4,0} = 5 (travel time from customer 4 to depot)
    • Arrival at depot: w0,2=w4,2+s4+t4,0=6+0+5=11w_{0,2} = w_{4,2} + s_4 + t_{4,0} = 6 + 0 + 5 = 11

Route 2 is also feasible! Decision variables:

  • x0,2,2=1x_{0,2,2} = 1, x2,4,2=1x_{2,4,2} = 1, x4,0,2=1x_{4,0,2} = 1
  • w2,2=3w_{2,2} = 3, w4,2=6w_{4,2} = 6

Step 4: Verifying All ConstraintsLink Copied

Let's verify that our complete solution satisfies all constraints:

Constraint 1 (Each customer visited exactly once):

For each customer iV{0}i \in V \setminus \{0\}, we verify:

kKjV,jixijk=1\sum_{k \in K} \sum_{j \in V, j \neq i} x_{ijk} = 1
  • Customer 1 (i=1i=1):
k{1,2}j{0,2,3,4}xj,1,k=x0,1,1+x0,1,2+x2,1,1+x2,1,2+=1+0+0+0+=1\sum_{k \in \{1,2\}} \sum_{j \in \{0,2,3,4\}} x_{j,1,k} = x_{0,1,1} + x_{0,1,2} + x_{2,1,1} + x_{2,1,2} + \cdots = 1 + 0 + 0 + 0 + \cdots = 1

(Only x0,1,1=1x_{0,1,1} = 1, all others are 0)

  • Customer 2 (i=2i=2):
k{1,2}j{0,1,3,4}xj,2,k=x0,2,1+x0,2,2+=0+1+=1\sum_{k \in \{1,2\}} \sum_{j \in \{0,1,3,4\}} x_{j,2,k} = x_{0,2,1} + x_{0,2,2} + \cdots = 0 + 1 + \cdots = 1

(Only x0,2,2=1x_{0,2,2} = 1)

  • Customer 3 (i=3i=3):

    k{1,2}j{0,1,2,4}xj,3,k=x1,3,1+x1,3,2+=1+0+=1\sum_{k \in \{1,2\}} \sum_{j \in \{0,1,2,4\}} x_{j,3,k} = x_{1,3,1} + x_{1,3,2} + \cdots = 1 + 0 + \cdots = 1

    (Only x1,3,1=1x_{1,3,1} = 1)

  • Customer 4 (i=4i=4):

k{1,2}j{0,1,2,3}xj,4,k=x2,4,1+x2,4,2+=0+1+=1\sum_{k \in \{1,2\}} \sum_{j \in \{0,1,2,3\}} x_{j,4,k} = x_{2,4,1} + x_{2,4,2} + \cdots = 0 + 1 + \cdots = 1

(Only x2,4,2=1x_{2,4,2} = 1)

Constraint 2 (Capacity):

For vehicle 1 (k=1k=1):

iV{0}dijV,jixijk=d1j1xj,1,1+d3j3xj,3,1\sum_{i \in V \setminus \{0\}} d_i \sum_{j \in V, j \neq i} x_{ijk} = d_1 \cdot \sum_{j \neq 1} x_{j,1,1} + d_3 \cdot \sum_{j \neq 3} x_{j,3,1}

Since x0,1,1=1x_{0,1,1} = 1 and x1,3,1=1x_{1,3,1} = 1 (and all other xijk=0x_{ijk} = 0 for vehicle 1):

=d1x0,1,1+d3x1,3,1=31+41=3+4=7Q1=10= d_1 \cdot x_{0,1,1} + d_3 \cdot x_{1,3,1} = 3 \cdot 1 + 4 \cdot 1 = 3 + 4 = 7 \leq Q_1 = 10

For vehicle 2 (k=2k=2):

iV{0}dijV,jixijk=d2j2xj,2,2+d4j4xj,4,2\sum_{i \in V \setminus \{0\}} d_i \sum_{j \in V, j \neq i} x_{ijk} = d_2 \cdot \sum_{j \neq 2} x_{j,2,2} + d_4 \cdot \sum_{j \neq 4} x_{j,4,2}

Since x0,2,2=1x_{0,2,2} = 1 and x2,4,2=1x_{2,4,2} = 1 (and all other xijk=0x_{ijk} = 0 for vehicle 2):

=d2x0,2,2+d4x2,4,2=21+31=2+3=5Q2=10= d_2 \cdot x_{0,2,2} + d_4 \cdot x_{2,4,2} = 2 \cdot 1 + 3 \cdot 1 = 2 + 3 = 5 \leq Q_2 = 10

Constraint 3 (Flow conservation): Already verified in Steps 2 and 3

Constraint 4 (Time windows): Already verified in Steps 2 and 3

Constraint 5 (Temporal precedence): Already verified in Steps 2 and 3

Constraint 6 (Binary variables): All xijkx_{ijk} are 0 or 1

Solution SummaryLink Copied

Our complete solution:

Route 1 (Vehicle 1): Depot → Customer 1 → Customer 3 → Depot

  • Arcs used: x0,1,1=1x_{0,1,1} = 1, x1,3,1=1x_{1,3,1} = 1, x3,0,1=1x_{3,0,1} = 1
  • Travel time calculation:
(i,j)routetij=t0,1+t1,3+t3,0=2+3+4=9\sum_{(i,j) \in \text{route}} t_{ij} = t_{0,1} + t_{1,3} + t_{3,0} = 2 + 3 + 4 = 9
  • Demand served:
i{1,3}di=d1+d3=3+4=7 units\sum_{i \in \{1,3\}} d_i = d_1 + d_3 = 3 + 4 = 7 \text{ units}
  • Arrival times: w1,1=2w_{1,1} = 2 (within window [a1,b1]=[1,4][a_1, b_1] = [1, 4]), w3,1=5w_{3,1} = 5 (within window [a3,b3]=[3,6][a_3, b_3] = [3, 6])

Route 2 (Vehicle 2): Depot → Customer 2 → Customer 4 → Depot

  • Arcs used: x0,2,2=1x_{0,2,2} = 1, x2,4,2=1x_{2,4,2} = 1, x4,0,2=1x_{4,0,2} = 1
  • Travel time calculation:
(i,j)routetij=t0,2+t2,4+t4,0=3+3+5=11\sum_{(i,j) \in \text{route}} t_{ij} = t_{0,2} + t_{2,4} + t_{4,0} = 3 + 3 + 5 = 11
  • Demand served:
i{2,4}di=d2+d4=2+3=5 units\sum_{i \in \{2,4\}} d_i = d_2 + d_4 = 2 + 3 = 5 \text{ units}
  • Arrival times: w2,2=3w_{2,2} = 3 (within window [a2,b2]=[2,5][a_2, b_2] = [2, 5]), w4,2=6w_{4,2} = 6 (within window [a4,b4]=[4,7][a_4, b_4] = [4, 7])

Total travel time (objective value):

kK(i,j)Acijxijk=9+11=20\sum_{k \in K} \sum_{(i,j) \in A} c_{ij} x_{ijk} = 9 + 11 = 20

where cij=tijc_{ij} = t_{ij} (using travel time as cost).

Out[8]:
Visualization
Network diagram showing two vehicle routes with annotated travel times, customer demands, and time windows for VRPTW worked example.
Spatial routes for the worked example showing both vehicle routes with travel times annotated on each arc. Route 1 (green) serves customers C1 and C3, while Route 2 (orange) serves C2 and C4. Each customer node shows its demand (d) and time window (TW), with arrival times displayed above.
Out[9]:
Visualization
Timeline chart showing vehicle schedules with shaded time window regions and vertical bars indicating actual arrival times at each customer.
Schedule timeline showing how each vehicle progresses through time. Shaded regions represent customer time windows, and vertical bars mark actual arrival times. All arrivals fall within their respective time windows, confirming the solution's feasibility.

The visualizations above show the complete worked example. The first figure displays the spatial routes with travel times annotated on each arc, customer demands (d), and time windows (TW). The second figure shows the timeline for how each vehicle progresses through time, with shaded regions representing time windows and vertical bars marking actual arrival times. Notice how all arrivals fall within their respective time windows, confirming the solution's feasibility.

Why This Solution WorksLink Copied

This example illustrates several key insights:

  1. Capacity constraints force customer partitioning: Since total demand (12) exceeds individual vehicle capacity (10), we need to use both vehicles. The capacity constraint guides how we group customers.

  2. Time windows influence sequencing: Customer 1's early window (1-4) makes it a natural first stop. Customer 4's later window (4-7) provides flexibility for later visits.

  3. Travel times affect feasibility: The sequence C1→C3 works because travel time 3 allows arrival at C3 (time 5) within its window [3,6]. A different sequence might violate time windows.

  4. Constraints are interdependent: We can't optimize routes independently. The capacity constraint affects which customers can be grouped, which affects sequencing options, which affects time window feasibility.

This small example demonstrates the complexity that emerges even with just 4 customers. Real-world problems with hundreds of customers require sophisticated algorithms to navigate this constraint space efficiently.

Implementation in Google OR-ToolsLink Copied

Now that we understand the mathematical formulation and have worked through a manual example, let's see how to implement VRPTW using Google OR-Tools. This implementation translates our mathematical constraints into code, allowing the solver to automatically find optimal or near-optimal solutions.

From Mathematics to Code: Mapping ConstraintsLink Copied

OR-Tools uses a constraint programming approach that implements our mathematical formulation through "dimensions" and "callbacks." Each constraint from our formulation maps to specific OR-Tools constructs:

  • Routing decisions (xijkx_{ijk}): Handled automatically by the routing model's internal variables
  • Time tracking (wikw_{ik}): Implemented through a "Time" dimension
  • Capacity constraints: Implemented through a "Capacity" dimension
  • Time windows: Set as bounds on the Time dimension
  • Flow conservation: Enforced automatically by the routing model structure

Let's build the implementation step by step, connecting each code section to its corresponding mathematical concept.

We'll implement VRPTW step by step, starting with the data model and then building the solver configuration. This approach helps understand how each component maps to the mathematical formulation.

In[10]:
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import numpy as np
import matplotlib.pyplot as plt

def create_data_model():
    """Create the data for the VRPTW problem.
    
    This function defines all the problem parameters that correspond to
    the mathematical formulation's input data.
    """
    data = {}
    
    # Distance matrix (travel times in minutes)
    # This corresponds to t_{ij} in our formulation
    data['distance_matrix'] = [
        [0, 2, 3, 4, 5, 6],  # Depot
        [2, 0, 2, 3, 4, 5],  # Customer 1
        [3, 2, 0, 2, 3, 4],  # Customer 2
        [4, 3, 2, 0, 2, 3],  # Customer 3
        [5, 4, 3, 2, 0, 2],  # Customer 4
        [6, 5, 4, 3, 2, 0]   # Customer 5
    ]
    
    # Time windows [earliest, latest] in minutes from start
    # This corresponds to [a_i, b_i] in our formulation
    data['time_windows'] = [
        (0, 480),    # Depot: 8-hour workday
        (60, 180),   # Customer 1: 1-3 hours
        (120, 240),  # Customer 2: 2-4 hours
        (180, 300),  # Customer 3: 3-5 hours
        (240, 360),  # Customer 4: 4-6 hours
        (300, 420)   # Customer 5: 5-7 hours
    ]
    
    # Demands - corresponds to d_i in our formulation
    data['demands'] = [0, 2, 3, 1, 2, 3]  # Depot has 0 demand
    
    # Vehicle data - corresponds to Q_k and K in our formulation
    data['vehicle_capacities'] = [8, 8]  # Two vehicles with capacity 8
    data['num_vehicles'] = 2
    
    # Service times - corresponds to s_i in our formulation
    data['service_times'] = [0, 10, 15, 10, 15, 10]  # Depot has 0 service time
    
    return data

Now we'll create the solver function that implements all the constraints from our mathematical formulation:

In[11]:
def solve_vrptw():
    """Solve the VRPTW problem using OR-Tools.
    
    This function implements the complete mathematical formulation,
    translating each constraint into OR-Tools constructs.
    """
    # Create data model (all our parameters)
    data = create_data_model()
    
    # Step 1: Create routing index manager
    # This sets up the node indexing system (V = {0, 1, ..., n})
    # and vehicle indexing (K = {1, 2, ..., m})
    manager = pywrapcp.RoutingIndexManager(
        len(data['distance_matrix']),  # Number of nodes |V|
        data['num_vehicles'],           # Number of vehicles |K|
        0  # Depot is at index 0
    )
    
    # Step 2: Create routing model
    # This initializes the constraint programming model that will
    # manage our decision variables x_{ijk}
    routing = pywrapcp.RoutingModel(manager)
    
    # Step 3: Define the objective function
    # This implements: min Σ_{k∈K} Σ_{(i,j)∈A} c_{ij} x_{ijk}
    def distance_callback(from_index, to_index):
        """Callback that returns the cost of traveling from one node to another.
        
        This corresponds to c_{ij} in our objective function.
        OR-Tools will call this function to evaluate arc costs.
        """
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return data['distance_matrix'][from_node][to_node]
    
    # Register the callback and set it as the arc cost evaluator
    # This tells OR-Tools to minimize total travel cost
    transit_callback_index = routing.RegisterTransitCallback(distance_callback)
    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
    
    # Step 4: Add capacity constraints (Constraint 2)
    # This implements: Σ_{i∈V\{0}} d_i Σ_{j≠i} x_{ijk} ≤ Q_k
    def demand_callback(from_index):
        """Callback that returns the demand at a node.
        
        This corresponds to d_i in our capacity constraint.
        """
        from_node = manager.IndexToNode(from_index)
        return data['demands'][from_node]
    
    # Register demand callback and add capacity dimension
    # This creates a cumulative dimension that tracks total demand
    # along each route and enforces the capacity limit Q_k
    demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
    routing.AddDimensionWithVehicleCapacity(
        demand_callback_index,
        0,  # null capacity slack (no buffer)
        data['vehicle_capacities'],  # Q_k: maximum capacity per vehicle
        True,  # start cumul to zero (vehicles start empty)
        'Capacity'  # dimension name for reference
    )
    
    # Step 5: Add time dimension and constraints (Constraints 4 and 5)
    # This implements the time tracking w_{ik} and enforces:
    # - a_i ≤ w_{ik} ≤ b_i (time windows)
    # - w_{ik} + s_i + t_{ij} ≤ w_{jk} when x_{ijk} = 1 (precedence)
    def time_callback(from_index, to_index):
        """Callback that returns the time to travel from one node to another.
        
        This accounts for both travel time t_{ij} and service time s_i,
        implementing the temporal logic: arrival_time + service + travel = next_arrival
        """
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        travel_time = data['distance_matrix'][from_node][to_node]  # t_{ij}
        service_time = data['service_times'][from_node]            # s_i
        return travel_time + service_time
    
    # Register time callback and add time dimension
    # This creates a cumulative dimension that tracks arrival times w_{ik}
    time_callback_index = routing.RegisterTransitCallback(time_callback)
    routing.AddDimension(
        time_callback_index,
        480,  # allow waiting time (vehicles can wait if they arrive early)
        480,  # maximum time per vehicle (8-hour workday)
        False,  # don't force start cumul to zero (vehicles can start at different times)
        'Time'  # dimension name
    )
    
    # Get the time dimension to set time window constraints
    time_dimension = routing.GetDimensionOrDie('Time')
    
    # Step 6: Set time window bounds (Constraint 4)
    # This implements: a_i ≤ w_{ik} ≤ b_i for all locations
    for location_idx, time_window in enumerate(data['time_windows']):
        index = manager.NodeToIndex(location_idx)
        # SetRange enforces the time window bounds
        time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])
    
    # Step 7: Set depot time windows for vehicles
    # Vehicles must start and end within depot hours
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        time_dimension.CumulVar(index).SetRange(
            data['time_windows'][0][0],  # earliest start
            data['time_windows'][0][1]    # latest end
        )
    
    # Note: Constraint 1 (each customer visited once) and Constraint 3 (flow conservation)
    # are automatically enforced by OR-Tools' routing model structure.
    # Constraint 5 (temporal precedence) is handled by the time dimension's
    # cumulative logic combined with the time_callback.
    # Constraint 6 (binary variables) is inherent in the routing model.
    
    # Step 8: Configure search strategy
    # OR-Tools uses heuristics and local search to find good solutions
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
    )
    search_parameters.local_search_metaheuristic = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
    )
    search_parameters.time_limit.seconds = 30
    
    # Step 9: Solve the problem
    # This is where OR-Tools explores the solution space, checking
    # all constraints and minimizing the objective function
    solution = routing.SolveWithParameters(search_parameters)
    
    return solution, routing, manager, data

Now let's solve the problem and extract the solution:

In[12]:
# Solve the problem
solution, routing, manager, data = solve_vrptw()

# Extract route information
routes = []
route_distances = []
total_distance = 0

if solution:
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        route = []
        route_distance = 0
        
        # Follow the route by reading NextVar values
        # This traces which arcs have x_{ijk} = 1
        while not routing.IsEnd(index):
            node_index = manager.IndexToNode(index)
            route.append(node_index)
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += routing.GetArcCostForVehicle(
                previous_index, index, vehicle_id
            )
        
        route.append(manager.IndexToNode(index))
        routes.append(route)
        route_distances.append(route_distance)
        total_distance += route_distance
    objective_value = solution.ObjectiveValue()
else:
    objective_value = None
    routes = []
    route_distances = []
    total_distance = 0

Display the solution results:

Out[13]:
Solution found!
Objective value (total distance): 21

Routes:
  Vehicle 1: Depot -> C3 -> C4 -> C5 -> Depot
    Distance: 14, Load: 6/8
  Vehicle 2: Depot -> C1 -> C2 -> Depot
    Distance: 7, Load: 5/8

Summary:
  Total distance: 21
  Vehicles used: 2/2
  Customers served: 5

The objective value represents the sum of all arc costs used in the routes, corresponding to our mathematical objective function minkK(i,j)Acijxijk\min \sum_{k \in K} \sum_{(i,j) \in A} c_{ij} x_{ijk}. The solution assigns customers to vehicles such that all capacity constraints are satisfied (each vehicle carries at most 8 units of demand) and all time windows are respected. Finding a feasible solution confirms that all constraints can be satisfied simultaneously for this problem instance.

Let's visualize the solution to see how vehicles are assigned to customers:

In[14]:
def visualize_solution(solution, routing, manager, data, routes):
    """Visualize the VRPTW solution.
    
    This visualization displays the routes found by the solver,
    showing which arcs are used (where x_{ijk} = 1) and how
    vehicles are assigned to customers.
    """
    fig, ax = plt.subplots(1, 1, figsize=(12, 8))
    
    # Customer locations (simplified 2D coordinates for visualization)
    locations = [
        (0, 0),    # Depot
        (2, 3),    # Customer 1
        (4, 1),    # Customer 2
        (6, 4),    # Customer 3
        (8, 2),    # Customer 4
        (10, 5)    # Customer 5
    ]
    
    # Plot depot
    ax.scatter(locations[0][0], locations[0][1], c='red', s=200, marker='s', 
               label='Depot', zorder=5)
    
    # Plot customers
    for i in range(1, len(locations)):
        ax.scatter(locations[i][0], locations[i][1], c='blue', s=100, 
                  label=f'Customer {i}' if i == 1 else "")
    
    # Draw routes
    colors = ['green', 'orange', 'purple']
    for vehicle_id, route in enumerate(routes):
        color = colors[vehicle_id]
        for i in range(len(route) - 1):
            start_pos = locations[route[i]]
            end_pos = locations[route[i + 1]]
            
            ax.plot([start_pos[0], end_pos[0]], [start_pos[1], end_pos[1]], 
                   color=color, linewidth=3, alpha=0.8, 
                   label=f'Vehicle {vehicle_id + 1}' if i == 0 else "")
            
            # Add arrow
            mid_x = (start_pos[0] + end_pos[0]) / 2
            mid_y = (start_pos[1] + end_pos[1]) / 2
            dx = end_pos[0] - start_pos[0]
            dy = end_pos[1] - start_pos[1]
            ax.arrow(mid_x, mid_y, dx*0.1, dy*0.1, head_width=0.2, head_length=0.2, 
                    fc=color, ec=color, alpha=0.8)
    
    # Add customer labels
    for i, (x, y) in enumerate(locations[1:], 1):
        ax.annotate(f'C{i}', (x, y), xytext=(5, 5), textcoords='offset points', 
                   fontsize=10, bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
    
    ax.set_xlabel('X Coordinate')
    ax.set_ylabel('Y Coordinate')
    ax.set_title('VRPTW Solution Visualization')
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

# Visualize the solution
if solution:
    visualize_solution(solution, routing, manager, data, routes)
Out[14]:
Visualization
Spatial map showing vehicle routes with different colored paths connecting depot to customer locations, demonstrating optimal VRPTW solution.
VRPTW solution visualization showing vehicle routes found by Google OR-Tools solver. Each colored line represents a different vehicle's route, starting and ending at the depot (red square). The routes demonstrate how the solver partitions customers across the fleet to minimize total travel distance while respecting capacity constraints and customer time windows. Customers appearing on the same colored path are served by a single vehicle, illustrating the routing decisions made by the optimization algorithm.

The visualization shows how vehicles are assigned to serve customers. Each colored line represents a vehicle's route, starting and ending at the depot (red square). The routes minimize total travel distance while respecting capacity constraints and time windows. Customers appearing on the same colored path are served by a single vehicle, demonstrating how the solver partitions customers across the fleet to achieve feasibility.

Alternative Implementation: Custom HeuristicLink Copied

While OR-Tools uses sophisticated constraint programming to find optimal or near-optimal solutions, it's instructive to implement a simple greedy heuristic. This approach makes decisions sequentially rather than exploring the full solution space, and it helps illustrate how the constraints guide decision-making. The heuristic doesn't explicitly solve for all xijkx_{ijk} variables simultaneously; instead, it builds routes incrementally, checking constraints as it goes. This is much faster but may not find optimal solutions.

The heuristic still respects the same constraints: each customer is visited exactly once (by construction), capacity limits are checked before adding customers, time windows are verified before assignment, and temporal logic is maintained through arrival time tracking.

In[15]:
def greedy_vrptw_heuristic(data):
    """Simple greedy heuristic for VRPTW.
    
    This heuristic builds routes incrementally by choosing the
    closest feasible customer at each step. It implements all major constraints
    but uses a greedy strategy rather than global optimization.
    
    Key insight: vehicles can WAIT if they arrive early at a customer.
    The arrival time becomes max(travel_arrival, window_start).
    """
    num_vehicles = data['num_vehicles']
    num_customers = len(data['distance_matrix']) - 1
    vehicle_capacities = data['vehicle_capacities']  # Q_k
    demands = data['demands']  # d_i (includes depot at index 0)
    time_windows = data['time_windows']  # [a_i, b_i] (includes depot)
    service_times = data['service_times']  # s_i
    distances = data['distance_matrix']  # t_{ij} (also used as travel times)
    depot_close = time_windows[0][1]  # Latest return time to depot
    
    # Initialize routes and tracking variables
    routes = [[] for _ in range(num_vehicles)]
    route_capacities = [0] * num_vehicles  # Cumulative demand (Constraint 2)
    route_current_times = [0] * num_vehicles  # Current time for each vehicle
    unvisited = list(range(1, num_customers + 1))  # Customers not yet assigned
    
    # Greedy assignment: repeatedly find the best customer-vehicle pair
    while unvisited:
        best_vehicle = None
        best_customer = None
        best_cost = float('inf')
        best_arrival_time = None
        
        for vehicle_id in range(num_vehicles):
            # Skip vehicles at capacity
            if route_capacities[vehicle_id] >= vehicle_capacities[vehicle_id]:
                continue
                
            for customer in unvisited:
                # Constraint 2: Check capacity feasibility
                if route_capacities[vehicle_id] + demands[customer] > vehicle_capacities[vehicle_id]:
                    continue
                
                # Calculate arrival time at this customer
                current_pos = 0 if not routes[vehicle_id] else routes[vehicle_id][-1]
                current_time = route_current_times[vehicle_id]
                travel_time = distances[current_pos][customer]
                earliest_arrival = current_time + travel_time
                
                # If we arrive early, we wait until window opens
                actual_arrival = max(earliest_arrival, time_windows[customer][0])
                
                # Constraint 4: Check if we can arrive before window closes
                if actual_arrival > time_windows[customer][1]:
                    continue
                
                # Check if we can return to depot on time after serving this customer
                departure_time = actual_arrival + service_times[customer]
                return_time = departure_time + distances[customer][0]
                if return_time > depot_close:
                    continue
                
                # Greedy choice: prefer closer customers (lower travel cost)
                cost = travel_time
                
                if cost < best_cost:
                    best_cost = cost
                    best_vehicle = vehicle_id
                    best_customer = customer
                    best_arrival_time = actual_arrival
        
        if best_vehicle is not None:
            # Add customer to route
            routes[best_vehicle].append(best_customer)
            route_capacities[best_vehicle] += demands[best_customer]
            # Update time: arrival + service time = ready for next customer
            route_current_times[best_vehicle] = best_arrival_time + service_times[best_customer]
            unvisited.remove(best_customer)
        else:
            break  # No feasible assignment found
    
    return routes, unvisited

# Test the heuristic with the same data as OR-Tools
data_heuristic = create_data_model()
heuristic_routes, unserved = greedy_vrptw_heuristic(data_heuristic)

# Calculate route statistics
heuristic_route_strings = []
heuristic_total_customers = 0
heuristic_total_distance = 0

for i, route in enumerate(heuristic_routes):
    if route:
        # Calculate total distance for this route
        route_distance = data_heuristic['distance_matrix'][0][route[0]]  # Depot to first
        for j in range(len(route) - 1):
            route_distance += data_heuristic['distance_matrix'][route[j]][route[j+1]]
        route_distance += data_heuristic['distance_matrix'][route[-1]][0]  # Last to depot
        
        route_str = f"Depot -> {' -> '.join([f'C{c}' for c in route])} -> Depot"
        heuristic_route_strings.append((i+1, route_str, len(route), route_distance))
        heuristic_total_customers += len(route)
        heuristic_total_distance += route_distance
    else:
        heuristic_route_strings.append((i+1, "No customers assigned", 0, 0))

Display the heuristic solution and compare with OR-Tools:

Out[16]:
Greedy Heuristic Solution:
--------------------------------------------------
Vehicle 1: Depot -> C1 -> C2 -> C3 -> C4 -> Depot
           (customers: 4, distance: 13)
Vehicle 2: Depot -> C5 -> Depot
           (customers: 1, distance: 12)

Summary:
  Customers served: 5/5
  Total distance: 25

Comparison with OR-Tools:
  OR-Tools distance: 21
  Heuristic distance: 25
  Optimality gap: 19.0%

The greedy heuristic builds routes incrementally by always choosing the nearest feasible customer. The key insight is that vehicles can wait if they arrive before a time window opens, so early arrival doesn't disqualify a customer. At each step, the algorithm checks: (1) capacity constraints, (2) time window feasibility, and (3) whether the vehicle can return to the depot on time after serving the customer.

Comparing the heuristic to OR-Tools reveals the cost of greedy decision-making. The heuristic finds a feasible solution quickly but typically produces routes that are longer than optimal. This is because locally optimal choices (picking the nearest customer) don't always lead to globally optimal solutions. OR-Tools explores the full solution space using constraint propagation and metaheuristics, finding better route combinations that the greedy approach misses.

Key ParametersLink Copied

Below are the main parameters that affect how the VRPTW solver works and performs.

  • first_solution_strategy: Heuristic for generating the initial solution. PATH_CHEAPEST_ARC (default) builds routes by selecting the cheapest arc at each step. SAVINGS uses Clarke-Wright savings algorithm. CHRISTOFIDES uses Christofides algorithm for TSP components.

  • local_search_metaheuristic: Metaheuristic for improving the initial solution. GUIDED_LOCAL_SEARCH (recommended) balances exploration and exploitation. TABU_SEARCH avoids revisiting solutions. SIMULATED_ANNEALING probabilistically accepts worse solutions to escape local optima.

  • time_limit.seconds: Maximum solver time (default: unlimited). Use 30-60 seconds for 50-100 customers, 2-5 minutes for larger problems, 10-30 seconds for real-time dispatch.

  • vehicle_capacities: List of capacity limits QkQ_k per vehicle. Supports homogeneous (all same) or heterogeneous fleets. Total capacity must exceed total customer demand.

  • distance_matrix: Travel times or distances tijt_{ij} between all locations. Use actual road network values rather than Euclidean distances. Should be symmetric for undirected problems.

  • time_windows: Tuples (earliest, latest) defining [ai,bi][a_i, b_i] for each location. Depot window defines operational hours. Customer windows must be reachable from depot.

  • service_times: Duration sis_i spent at each location (excluding travel). Use realistic estimates. Underestimating causes downstream time window violations. Depot service time is typically 0.

  • demands: Demand did_i at each location. Depot has demand 0. Total demand should be less than total vehicle capacity with margin.

Key MethodsLink Copied

The following are the most commonly used methods for working with OR-Tools routing models.

  • RoutingIndexManager(num_nodes, num_vehicles, depot): Creates an index manager mapping between node indices and routing indices. The depot (typically 0) serves as start/end point for all routes.

  • RoutingModel(manager): Creates the main routing model that manages decision variables and constraints.

  • RegisterTransitCallback(callback): Registers a binary callback returning cost/time to travel between two nodes. Used for distance and time dimensions.

  • RegisterUnaryTransitCallback(callback): Registers a unary callback for single-node operations like demand pickup. Used for capacity dimensions.

  • AddDimension(callback, slack_max, capacity, fix_start_cumul_to_zero, name): Adds a cumulative dimension (time, capacity) to the model. The callback defines accumulation; slack_max and capacity define bounds.

  • AddDimensionWithVehicleCapacity(callback, slack, capacities, fix_start_cumul_to_zero, name): Like AddDimension but with per-vehicle capacity limits. Use for heterogeneous fleets.

  • SolveWithParameters(search_parameters): Solves the problem with specified search parameters. Returns solution object or None if infeasible.

  • solution.ObjectiveValue(): Returns the objective value (total cost) of the solution: kK(i,j)Acijxijk\sum_{k \in K} \sum_{(i,j) \in A} c_{ij} x_{ijk}.

  • solution.Value(routing.NextVar(index)): Returns the next node in the route. Use to trace complete routes for each vehicle.

Practical ImplicationsLink Copied

VRPTW addresses logistics scenarios where customers have specific availability windows that constrain when service can occur. This makes it appropriate for last-mile delivery operations, field service scheduling, healthcare logistics, and any routing problem where temporal constraints are binding rather than advisory. The formulation captures the core tradeoff between route efficiency and schedule adherence that characterizes modern logistics operations.

The approach is most valuable when time windows represent hard constraints, situations where arriving outside the window is genuinely infeasible rather than merely undesirable. Examples include scheduled appointment services, store receiving hours, and perishable goods delivery where timing affects product quality. When time windows are soft (violations incur penalties but are permitted), the standard VRPTW formulation requires modification to include penalty terms in the objective function. For problems without meaningful temporal constraints, simpler VRP variants will be more appropriate and computationally tractable.

Consider the density and overlap of time windows when assessing problem suitability. Problems with highly restrictive, non-overlapping windows may have few or no feasible solutions, while problems with very wide windows offer little value from the time window constraint and may be better modeled as basic VRP. The interaction between window tightness and geographic dispersion determines problem difficulty: clustered customers with overlapping windows are easier than dispersed customers with tight, disjoint windows.

Best PracticesLink Copied

Configure OR-Tools with PATH_CHEAPEST_ARC as the initial solution strategy and GUIDED_LOCAL_SEARCH as the metaheuristic for general-purpose VRPTW solving. For problems dominated by capacity constraints, consider SAVINGS instead. Set time_limit.seconds based on operational requirements: 30-60 seconds provides good solutions for 50-100 customer problems, while 2-5 minutes yields near-optimal solutions for batch planning. Real-time dispatch applications may need 10-30 second limits, accepting some solution quality degradation.

Validate problem feasibility before invoking the solver by checking that total demand does not exceed total vehicle capacity and that each customer can be reached from the depot within its time window. This prevents wasted computation on infeasible instances. For problems exceeding 100 customers, apply geographic clustering or time-window-based partitioning to decompose the problem into tractable subproblems. Solve subproblems independently, then optionally refine inter-cluster connections.

Use conservative service time estimates rather than optimistic ones. Underestimating service duration propagates delays through the route, causing downstream time window violations that may not appear until execution. When service times vary significantly, model the 75th-90th percentile duration rather than the mean.

Data Requirements and PreprocessingLink Copied

The travel time matrix is the most critical input, as errors directly translate to infeasible solutions or poor execution. Use routing APIs or historical GPS data to estimate actual driving times rather than computing Euclidean or Manhattan distances. Traffic patterns vary by time of day, so consider using time-dependent travel times for problems spanning multiple hours. For problems with 100+ customers, the matrix requires n2n^2 storage; consider sparse representations or on-demand distance computation for larger instances.

Time window data must be validated against travel time feasibility. For each customer, verify that the depot-to-customer travel time allows arrival within the window, accounting for earliest departure from the depot. Similarly, check that customer-to-depot return is possible within operational hours. Customers failing these checks indicate data errors or require special handling (e.g., dedicated vehicles, earlier depot opening).

Demand data should sum to less than total vehicle capacity with reasonable margin. If total demand approaches total capacity, the problem becomes tightly constrained and may require all vehicles regardless of route efficiency. Service times should include all activities at the customer location: parking, walking, handoff, signature collection, and departure preparation. Depot service time is typically zero unless vehicles require loading or inspection.

Common PitfallsLink Copied

Relying on Euclidean distances for the travel matrix creates solutions that appear optimal but fail during execution. Road networks, traffic lights, and restricted turns can double or triple actual travel time compared to straight-line distance. Validate computed routes against actual driving times for a sample of arcs and adjust the matrix if systematic errors exist.

Setting time windows without considering travel time dependencies creates infeasible instances. If customer A's window closes at 10:00 and customer B's window opens at 10:15, but travel between them requires 30 minutes, no single vehicle can serve both. These hidden conflicts may not be obvious until the solver reports infeasibility or produces poor solutions.

Ignoring service time variability leads to optimistic schedules. If actual service takes longer than modeled, delays accumulate along the route. A 5-minute underestimate at each of 8 customers creates 40 minutes of delay by route end. Model conservatively, especially for variable-duration services like installation or repair.

Computational ConsiderationsLink Copied

VRPTW solution time grows exponentially with problem size due to NP-hardness. Practical scaling guidelines:

Customer CountApproachTypical TimeMemory (Distance Matrix)
< 50Exact (branch-and-bound)1-5 minutes< 10 KB
50-100Heuristic (OR-Tools)30-120 seconds< 40 KB
100-200Heuristic with decomposition2-5 minutes< 160 KB
200-500Geographic partitioning5-15 minutes< 1 MB
> 500Hierarchical approaches15+ minutes1+ MB

The solver's memory footprint beyond the distance matrix is modest, typically under 100 MB for problems up to 500 customers. For larger problems, the primary bottleneck is solution time rather than memory. Decomposition strategies (clustering customers by location or time window) offer the best path to scaling, as they convert one large problem into several smaller, independently solvable problems.

Performance and Deployment ConsiderationsLink Copied

Evaluate solutions using multiple metrics: total travel distance (efficiency), number of vehicles used (resource utilization), time window adherence rate (service quality), and computation time (responsiveness). No single metric captures solution quality. A solution minimizing distance may require more vehicles; one using fewer vehicles may have tighter timing margins.

Production systems should distinguish batch and real-time scenarios. Batch planning (overnight route computation) can allocate 5-10 minutes per solve for near-optimal solutions. Real-time dispatch (responding to new orders) needs 10-30 second limits, using incremental re-optimization that modifies existing routes rather than solving from scratch.

Implement solution validation before deployment: programmatically verify that each route satisfies capacity constraints, arrives within time windows, and allows timely depot return. Solvers occasionally return solutions that violate constraints due to numerical issues or callback errors. For robustness, maintain fallback strategies when the solver fails. Options include relaxing the tightest time windows, deploying additional vehicles, or flagging problematic customers for manual handling.

SummaryLink Copied

The Vehicle Routing Problem with Time Windows represents a critical optimization challenge in modern logistics, balancing operational efficiency with customer service requirements. Through mathematical formulation and advanced optimization techniques, we can find solutions that minimize travel costs while respecting vehicle capacity and time window constraints.

Google OR-Tools provides powerful, accessible tools for solving VRPTW problems, making advanced optimization techniques available to practitioners without deep expertise in operations research. The combination of exact methods for smaller problems and heuristic approaches for larger instances enables practical application across various industries and problem sizes.

Successful VRPTW implementation requires careful attention to data quality, system integration, and ongoing optimization. The mathematical foundation provides a solid basis for understanding problem complexity, while modern computational tools make practical application feasible for real-world logistics operations.

QuizLink Copied

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about the Vehicle Routing Problem with Time Windows.

Loading component...

Reference

BIBTEXAcademic
@misc{vehicleroutingproblemwithtimewindowscompleteguidetovrptwoptimizationwithortools, author = {Michael Brenndoerfer}, title = {Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools}, year = {2025}, url = {https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide}, organization = {mbrenndoerfer.com}, note = {Accessed: 2025-12-07} }
APAAcademic
Michael Brenndoerfer (2025). Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools. Retrieved from https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide
MLAAcademic
Michael Brenndoerfer. "Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools." 2025. Web. 12/7/2025. <https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide>.
CHICAGOAcademic
Michael Brenndoerfer. "Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools." Accessed 12/7/2025. https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools'. Available at: https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide (Accessed: 12/7/2025).
SimpleBasic
Michael Brenndoerfer (2025). Vehicle Routing Problem with Time Windows: Complete Guide to VRPTW Optimization with OR-Tools. https://mbrenndoerfer.com/writing/vehicle-routing-problem-time-windows-vrptw-optimization-guide
Michael Brenndoerfer

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.

Stay updated

Get notified when I publish new articles on data and AI, private equity, technology, and more.