Capacitated Vehicle Routing Problem (CVRP)

The Capacitated Vehicle Routing Problem (CVRP) aims to find a set of routes for $V$ vehicles that start and end at a single depot and collectively serve all customers. We index the locations by $i \in \lbrace 0,1,\ldots,N-1\rbrace$, where location 0 denotes the depot and locations $1,\ldots,N-1$ are customers. Each customer $i\in \lbrace 1,\ldots,N-1\rbrace$ has a demand $d_i$ to be delivered (and we set $d_0=0$ for the depot). Each vehicle $v \in \lbrace 0,\ldots,V-1\rbrace$ departs from the depot, visits a subset of customers, and returns to the depot, subject to the capacity constraint that the total delivered demand on its route does not exceed the vehicle capacity $q_v$. The objective is to minimize the total travel cost over all vehicles.

We assume that locations are points in the two-dimensional plane and that the travel cost between two locations is the Euclidean distance. Let $c_{i,j}$ denote the distance (cost) between locations $i$ and $j$.

QUBO++ formulation: array of binary variables

We give each vehicle $L$ slots, where slot $t$ of vehicle $v$ holds the $t$-th customer visited by vehicle $v$. A slot may also be empty, which we represent by assigning the depot (location 0) to it: an empty slot simply means that the vehicle serves fewer than $L$ customers.

The number of slots $L$ is an upper bound on the number of customers a single vehicle can serve. It is computed in advance from the demands and the largest vehicle capacity: sort the demands in ascending order and greedily accumulate them until the largest capacity is exceeded; the number of accumulated demands is $L$. For the instance solved below, we have $L=9$, so the formulation uses $V\times L\times N = 3\times 9\times 15 = 405$ binary variables.

We thus use a $V\times L\times N$ array $A=(a_{v,t,i})$ ($0\leq v\leq V-1$, $0\leq t\leq L-1$, $0\leq i\leq N-1$) of binary variables, where $a_{v,t,i}$ is 1 if and only if slot $t$ of vehicle $v$ holds location $i$ (with $i=0$ meaning that the slot is empty).

The figure below shows an example assignment of $A=(a_{v,t,i})$ for the $V=3$, $N=15$, $L=9$ instance (the instance solved by the program below), representing a CVRP solution. For each vehicle (0, 1, 2) it shows an $L\times N$ grid; a colored cell is $a_{v,t,i}=1$ (slot $t$ of vehicle $v$ holds location $i$). Slots assigned to the depot (location 0) are empty slots.

assignment array a_{v,t,i}

Each vehicle starts at the depot (location 0), visits the customers stored in its non-empty slots in order, and returns to the depot. Each customer $1,\ldots,14$ appears in exactly one slot of exactly one vehicle, so this array represents a feasible CVRP solution. The empty slots line up after the customers, but the formulation does not require this: an empty slot between two customers only means the vehicle passes through the depot in between, which never shortens a route under the triangle inequality. Hence no extra constraint on the position of empty slots is needed, and the optimal solution is naturally a lean set of routes.

Constraints for QUBO++ formulation

Row constraint (one-hot at each slot)

Each slot must hold exactly one location (a customer or the depot for an empty slot). We impose the one-hot constraint:

\[\begin{aligned} \text{row}\_\text{constraint} & = \sum_{v=0}^{V-1}\sum_{t=0}^{L-1}\bigr(\sum_{i=0}^{N-1} a_{v,t,i} = 1\bigl)\\ &= \sum_{v=0}^{V-1}\sum_{t=0}^{L-1}\bigr(1-\sum_{i=0}^{N-1} a_{v,t,i}\bigl)^2 \end{aligned}\]

row_constraint attains its minimum value $0$ if and only if every row is one-hot.

Column constraint

Each customer must be held by exactly one slot of exactly one vehicle:

\[\begin{aligned} \text{column}\_\text{constraint} & = \sum_{i=1}^{N-1}\bigr(\sum_{v=0}^{V-1}\sum_{t=0}^{L-1} a_{v,t,i} = 1\bigl)\\ &= \sum_{i=1}^{N-1}\bigr(1-\sum_{v=0}^{V-1}\sum_{t=0}^{L-1} a_{v,t,i}\bigl)^2 \end{aligned}\]

column_constraint is 0 if and only if every customer $i = 1, \dots ,N−1$ is visited exactly once. Note that no such constraint is imposed on the depot column $i=0$: any number of slots may be empty.

Capacity constraint

For each vehicle $v$, the total delivered demand is

\[\sum_{t=0}^{L-1}\sum_{i=1}^{N-1}d_ia_{v,t,i},\]

which must be at most $q_v$. Then the following constraint must be 0:

\[\begin{aligned} \text{capacity}\_\text{constraint} &= \sum_{v=0}^{V-1}\Bigr(0\leq \sum_{t=0}^{L-1}\sum_{i=1}^{N-1}d_ia_{v,t,i}\leq q_v\Bigl) \end{aligned}\]

capacity_constraint is 0 if and only if all vehicles do not exceed their capacity.

Objective for QUBO formulation

The total tour cost of a vehicle consists of the leg from the depot to its first slot, the legs between consecutive slots, and the leg from its last slot back to the depot:

\[\begin{aligned} \text{objective} &= \sum_{v=0}^{V-1}\Bigr(\sum_{i=1}^{N-1}c_{0,i}a_{v,0,i} + \sum_{t=0}^{L-2}\sum_{i=0}^{N-1}\sum_{j=0}^{N-1}c_{i,j}a_{v,t,i}a_{v,t+1,j} + \sum_{i=1}^{N-1}c_{i,0}a_{v,L-1,i}\Bigl) \end{aligned}\]

Under the Euclidean metric we have $c_{i,i}=0$, so empty slots at the beginning or the end of a route contribute no extra cost, and when all constraints are satisfied $\text{objective}$ equals the total travel cost of all vehicles.

QUBO formulation for the CVRP

Combining the objective and constraints, we obtain the QUBO:

\[\begin{aligned} f &= \text{objective} + P\cdot \text{cons}(\text{row}\_\text{constraint}+\text{column}\_\text{constraint}+\text{capacity}\_\text{constraint}), \end{aligned}\]

where $P$ is the constraint weight. Wrapping the constraint part in qbpp::cons() declares it as constraints; the solver then searches efficiently for solutions satisfying them (see Native Constraints).

QUBO++ program

The following QUBO++ program finds a solution to a randomly generated CVRP instance with $N=15$ locations (a depot and 14 customers) and $V=3$ vehicles, with a time limit of 10 seconds. The vector locations stores triples (x,y,d), where (x,y) is the 2D coordinate of a location and d is a customer’s demand (the depot demand is 0). The vector vehicle_capacity stores the capacities {200, 250, 300} of the $V=3$ vehicles.

Distances are the exact Euclidean distances computed with std::sqrt (no rounding). Since the default coeff_t is an integer type, we define DOUBLE_TYPE before including the header to use the real (double) coefficient frontend and build objective in double.

#define DOUBLE_TYPE
#include <algorithm>
#include <cmath>
#include <qbpp/qbpp.hpp>
#include <qbpp/easy_solver.hpp>
#include <qbpp/graph.hpp>

int main() {
  std::vector<std::tuple<float, float, int>> locations = {
      {200, 200, 0},  {330, 320, 38}, {17, 390, 25},  {57, 352, 13},
      {79, 233, 95},  {9, 316, 16},   {397, 279, 48}, {251, 348, 32},
      {258, 157, 63}, {3, 215, 31},   {214, 107, 48}, {389, 9, 80},
      {106, 371, 61}, {198, 314, 47}, {315, 155, 76}};
  std::vector<int> vehicle_capacity = {200, 250, 300};

  const size_t N = locations.size();
  const size_t V = vehicle_capacity.size();

  auto dist = [&](size_t i, size_t j) {
    const auto [x1, y1, q1] = locations[i];
    const auto [x2, y2, q2] = locations[j];
    return std::sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  };

  std::vector<int> sorted_demands;
  for (size_t i = 1; i < N; ++i)
    sorted_demands.push_back(std::get<2>(locations[i]));
  std::sort(sorted_demands.begin(), sorted_demands.end());
  const int max_capacity =
      *std::max_element(vehicle_capacity.begin(), vehicle_capacity.end());
  size_t L = 0;
  for (int acc = 0; L < sorted_demands.size() &&
                    acc + sorted_demands[L] <= max_capacity;
       ++L) {
    acc += sorted_demands[L];
  }

  auto a = qbpp::var("a", V, L, N);

  auto row_constraint = qbpp::sum(qbpp::vector_sum(a) == 1);

  auto column_sum = qbpp::expr(N - 1);
  for (size_t v = 0; v < V; ++v)
    for (size_t t = 0; t < L; ++t)
      for (size_t i = 1; i < N; ++i) column_sum[i - 1] += a[v][t][i];
  auto column_constraint = qbpp::sum(column_sum == 1);

  auto vehicle_load = qbpp::expr(V);
  auto capacity_constraint = qbpp::toExpr(0);
  for (size_t v = 0; v < V; ++v) {
    for (size_t t = 0; t < L; ++t)
      for (size_t i = 1; i < N; ++i)
        vehicle_load[v] += a[v][t][i] * std::get<2>(locations[i]);
    capacity_constraint += 0 <= vehicle_load[v] <= vehicle_capacity[v];
  }

  auto objective = qbpp::toExpr(0);
  for (size_t v = 0; v < V; ++v) {
    for (size_t i = 1; i < N; ++i) objective += dist(0, i) * a[v][0][i];
    for (size_t t = 0; t + 1 < L; ++t)
      for (size_t i = 0; i < N; ++i)
        for (size_t j = 0; j < N; ++j)
          if (dist(i, j) != 0) objective += dist(i, j) * a[v][t][i] * a[v][t + 1][j];
    for (size_t i = 1; i < N; ++i) objective += dist(i, 0) * a[v][L - 1][i];
  }

  auto f = objective + 3000 * qbpp::cons(row_constraint + column_constraint +
                                         capacity_constraint);
  f.simplify_as_binary();
  auto solver = qbpp::EasySolver(f);
  auto sol = solver.search({{"time_limit", 10}});

  std::cout << "violated constraints = " << f.cons(sol) << std::endl;
  std::cout << "objective = " << objective(sol) << std::endl;

  auto tour = qbpp::onehot_to_int(sol(a));
  for (size_t v = 0; v < V; ++v) {
    std::cout << "Vehicle " << v << " : load = " << vehicle_load[v](sol)
              << " / " << vehicle_capacity[v] << " : 0 ";
    for (size_t t = 0; t < L; ++t) {
      int node = tour[v][t];
      if (node > 0)
        std::cout << "-> " << node << "("
                  << std::get<2>(locations[static_cast<size_t>(node)]) << ") ";
    }
    std::cout << "-> 0" << std::endl;
  }

  qbpp::graph::GraphDrawer graph;
  for (size_t i = 0; i < locations.size(); ++i) {
    const auto [x, y, q] = locations[i];
    graph.add(qbpp::graph::Node(i).position(x, y).xlabel(
        q != 0 ? std::to_string(q) : ""));
  }
  for (size_t v = 0; v < V; ++v) {
    int prev = 0;
    for (size_t t = 0; t < L; ++t) {
      int node = tour[v][t];
      if (node <= 0) continue;
      graph.add(qbpp::graph::Edge(prev, node).directed().color(v + 1).penwidth(2.0f));
      prev = node;
    }
    if (prev != 0)
      graph.add(qbpp::graph::Edge(prev, 0).directed().color(v + 1).penwidth(2.0f));
  }
  graph.draw();
  graph.write("cvrp15.svg");
  return 0;
}

The program first computes the number of slots L (9 for this instance) from the sorted demands and the maximum vehicle capacity, and defines the array a of $V\times L\times N = 3\times 9\times 15 = 405$ binary variables. It then defines the objective term objective and the constraint terms row_constraint, column_constraint, and capacity_constraint, declares them as constraints with qbpp::cons(), and combines them: f = objective + 3000 * qbpp::cons(row_constraint + column_constraint + capacity_constraint). The Easy Solver then searches for an assignment sol that minimizes f with a time limit of 10 seconds.

f.cons(sol) returns the number of violated constraints (0 when all are satisfied). As an example, the following results are obtained:

violated constraints = 0
objective = 1821.13
Vehicle 0 : load = 165 / 200 : 0 -> 6(48) -> 1(38) -> 7(32) -> 13(47) -> 0
Vehicle 1 : load = 241 / 250 : 0 -> 4(95) -> 9(31) -> 5(16) -> 2(25) -> 3(13) -> 12(61) -> 0
Vehicle 2 : load = 267 / 300 : 0 -> 10(48) -> 11(80) -> 14(76) -> 8(63) -> 0

The total travel cost of 1821.13 is the best value confirmed by repeated long runs on this instance (we do not prove optimality). Being a heuristic solver, a 10-second run may end slightly above 1821.13 on occasion.

Finally, the program visualizes the obtained solution as a graph and writes it to cvrp15.svg:

A solution of the N=15 CVRP instance.


Back to top

Page last modified: 2026.07.09.