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 use a $V\times N\times N$ array $A=(a_{v,t,i})$ ($0\leq v\leq V, 0\leq t,i\leq N-1$) of binary variables to formulate the CVRP as a QUBO expression, where $a_{v,t,i}$ is 1 if and only if the $t$-th visited location of vehicle $v$ is location $i$.

Below is an example assignment of $A=(a_{v,t,i})$ with $V=3$ and $N=10$ representing a solution of the CVRP. Each vehicle starts from the depot (location 0), visits some customers, and then returns to the depot. After a vehicle returns to the depot, it stays at the depot for the remaining time steps. Each customer $1, \ldots, 9$ is visited exactly once by exactly one vehicle, so this array represents a feasible CVRP solution.

Vehicle $v=0$

Representing tour: $0\rightarrow 4\rightarrow 0$

t \ i 0 1 2 3 4 5 6 7 8 9 onehot value
0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 1 0 0 0 0 0 4
2 1 0 0 0 0 0 0 0 0 0 0
3 1 0 0 0 0 0 0 0 0 0 0
4 1 0 0 0 0 0 0 0 0 0 0
5 1 0 0 0 0 0 0 0 0 0 0
6 1 0 0 0 0 0 0 0 0 0 0
7 1 0 0 0 0 0 0 0 0 0 0
8 1 0 0 0 0 0 0 0 0 0 0
9 1 0 0 0 0 0 0 0 0 0 0

Vehicle $v=1$

Representing tour: $0\rightarrow 6\rightarrow 5\rightarrow 8\rightarrow 0$

t \ i 0 1 2 3 4 5 6 7 8 9 onehot value
0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 1 0 0 0 6
2 0 0 0 0 0 1 0 0 0 0 5
3 0 0 0 0 0 0 0 0 1 0 8
4 1 0 0 0 0 0 0 0 0 0 0
5 1 0 0 0 0 0 0 0 0 0 0
6 1 0 0 0 0 0 0 0 0 0 0
7 1 0 0 0 0 0 0 0 0 0 0
8 1 0 0 0 0 0 0 0 0 0 0
9 1 0 0 0 0 0 0 0 0 0 0

Vehicle $v=2$

Representing tour: $0\rightarrow 7\rightarrow 9\rightarrow 1\rightarrow 3\rightarrow 2\rightarrow 0$

t \ i 0 1 2 3 4 5 6 7 8 9 onehot value
0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 1 0 0 7
2 0 0 0 0 0 0 0 0 0 1 9
3 0 1 0 0 0 0 0 0 0 0 1
4 0 0 0 1 0 0 0 0 0 0 3
5 0 0 1 0 0 0 0 0 0 0 2
6 1 0 0 0 0 0 0 0 0 0 0
7 1 0 0 0 0 0 0 0 0 0 0
8 1 0 0 0 0 0 0 0 0 0 0
9 1 0 0 0 0 0 0 0 0 0 0

Constraints for QUB++ formulation

Row constraint (one-hot at each time)

Each vehicle must be at exactly one location at each time $t$. We impose the one-hot constraint:

\[\begin{aligned} \text{row}\_\text{constraint} & = \sum_{v=0}^{V-1}\sum_{t=0}^{N-1}\bigr(\sum_{i=0}^{N-1} a_{v,t,i} = 1\bigl)\\ &= \sum_{v=0}^{V-1}\sum_{t=0}^{N-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.

We also fix the first position to be the depot:

\[\begin{aligned} a_{v,0,0}& = 1 && (0\leq v\leq V-1) \\ a_{v,0,i}& = 0 && (0\leq v\leq V-1, 1\leq i\leq N-1) \end{aligned}\]

These can be enforced by fixing variables.

Consecutive-depot constraint (no “leaving the depot again”)

We disallow patterns where a vehicle returns to the depot and later visits customers again. Using the depot-indicator variable $a_{v,t,0}$, we require that the sequence

\[a_{v,1,0}, a_{v,2,0}, \ldots, a_{v,N-1,0}\]

consists of consecutive 0’s followed by consecutive 1’s (i.e., once it becomes 1, it stays 1). This is enforced by penalizing the forbidden transition $1\rightarrow 0$:

\[\begin{aligned} \text{consecutive}\_\text{constraint} &= \sum_{v=0}^{V-1}\sum_{t=1}^{N-2} (1-a_{v,t})a_{v,t+1} \end{aligned}\]

consecutive_constraint attains 0 if and only if the depot indicators are monotone nondecreasing in $t$ ($1\leq t\leq N-1$).

This constraint is redundant if the distance metric satisfies the triangle inequality (such “leave again” solutions cannot be optimal), but it often helps solvers avoid such non-optimal structures.

Column constraint

Each customer must be visited exactly once by exactly one vehicle at exactly one time:

\[\begin{aligned} \text{column}\_\text{constraint} & = \sum_{i=1}^{N-1}\bigr(\sum_{v=0}^{V-1}\sum_{t=0}^{N-1} a_{v,t,i} = 1\bigl)\\ &= \sum_{i=1}^{N-1}\bigr(1-\sum_{v=0}^{V-1}\sum_{t=0}^{N-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.

Capacity constraint

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

\[\sum_{t=1}^{N-1}\sum_{i=1}^{N-1}d_ix_{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=1}^{N-1}\sum_{i=1}^{N-1}d_ix_{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 is computed from consecutive visited locations:

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

If vehicle $v$ visits location $i$ at time step $t$ and then moves to location $j$ at time step $(t+1)\bmod N$, we have $x_{v,t,i}=x_{v,(t+1)\bmod N, j}=1$. In this case, the corresponding term contributes $c_{i,j}$ to the sum. Therefore, when all constraints are satisfied (so that each $(v,t)$ has exactly one active location), $\text{objective}$ equals the total travel cost of all vehicles.

Under the Euclidean metric, we have $c_{i,i}=0$ for all $i$, so staying at the same location does not add extra cost.

QUBO formulation for the CVRP

Combining the objective and constraints, we obtain the QUBO:

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

where $P$ is a sufficiently large constant so that feasibility (constraints) is prioritized over cost minimization.

QUBO++ program

The following QUBO++ program finds a solution to the CVRP instance with $N=10$ locations and $V=3$ vehicles. The vector locations stores triples (x,y,d), where (x,y) is the 2D coordinate of the location and d is the customer demand (with demand 0 for the depot). The vector vehicle_capacity stores the capacities of the $V=3$ vehicles. In this example, it is set to {100, 200, 300}, so vehicles 0, 1, and 2 have small, medium, and large capacities, respectively.

#include "qbpp.hpp"
#include "qbpp_easy_solver.hpp"
#include "qbpp_graph.hpp"

int main() {
  std::vector<std::tuple<float, float, int>> locations = {
      {200, 200, 0},  {247, 296, 44}, {31, 393, 57}, {96, 398, 94},
      {391, 230, 91}, {118, 95, 66},  {197, 99, 59}, {224, 8, 10},
      {3, 10, 52},    {281, 379, 83}};
  std::vector<int> vehicle_capacity = {100, 200, 300};

  const size_t N = locations.size();

  const size_t V = vehicle_capacity.size();

  auto a = qbpp::var("a", V, N, 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 < N; ++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 consecutive_constraint = qbpp::toExpr(0);
  for (size_t v = 0; v < V; ++v) {
    for (size_t t = 1; t < N - 1; ++t) {
      consecutive_constraint += a[v][t][0] * (1 - a[v][t + 1][0]);
    }
  }

  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 < N; ++t) {
      for (size_t i = 1; i < N; ++i) {
        const auto [x, y, q] = locations[i];
        vehicle_load[v] += a[v][t][i] * q;
      }
    }
    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 t = 0; t < N; ++t) {
      auto next_t = (t + 1) % N;
      for (size_t i = 0; i < N; ++i) {
        const auto [x1, y1, q1] = locations[i];
        for (size_t j = 0; j < N; ++j) {
          const auto [x2, y2, q2] = locations[j];
          int dist = static_cast<int>(
              std::sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)));
          objective += dist * a[v][t][i] * a[v][next_t][j];
        }
      }
    }
  }

  auto f = objective + 100000 * (row_constraint + column_constraint +
                                 consecutive_constraint + capacity_constraint);

  qbpp::MapList ml;
  for (size_t v = 0; v < V; ++v) {
    ml.push_back({a[v][0][0], 1});
    for (size_t i = 1; i < N; ++i) {
      ml.push_back({a[v][0][i], 0});
    }
  }

  auto g = qbpp::replace(f, ml);
  f.simplify_as_binary();
  g.simplify_as_binary();
  auto solver = qbpp::easy_solver::EasySolver(g);

  auto sol = solver.search();

  auto full_sol = qbpp::Sol(f).set(sol).set(ml);

  std::cout << "row_constraint = " << row_constraint(full_sol) << std::endl;
  std::cout << "column_constraint = " << column_constraint(full_sol)
            << std::endl;
  std::cout << "consecutive_constraint = " << consecutive_constraint(full_sol)
            << std::endl;
  std::cout << "capacity_constraint = " << capacity_constraint(full_sol)
            << std::endl;
  std::cout << "objective = " << objective(full_sol) << std::endl;

  auto tour = qbpp::onehot_to_int(full_sol(a));

  for (size_t v = 0; v < V; ++v) {
    std::cout << "Vehicle " << v << " : load = " << vehicle_load[v](full_sol)
              << " / " << vehicle_capacity[v];
    std::cout << " : 0 ";
    for (size_t t = 1; t < N; ++t) {
      auto 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) {
    for (size_t i = 0; i < N; ++i) {
      int from = tour[v][i];
      int to = tour[v][(i + 1) % N];
      if (from < 0 || to < 0 || from == to) continue;
      graph.add(qbpp::graph::Edge(from, to).color(v + 1).penwidth(2.0f));
    }
  }
  graph.draw();
  graph.write("cvrp.svg");
}

This program defines a $V\times N\times N$ array a of binary variables. It then defines the objective term objective and the constraint terms row_constraint, column_constraint. consecutive_constraint and capacity_constraint according to the formulation described above, and combines them into the penalized objective f = objective + 100000 * ( row_constraint + column_constraint + consecutive_constraint + capacity_constraint ).

To fix the first visited location ($t=0$) of every vehicle to the depot (location 0), the program constructs a mapping ml and applies it to f using qbpp::replace(). The resulting expression is stored in g. The Easy Solver is then used to search for an assignment sol that minimizes g.

Since g does not contain the variables fixed by ml, the program merges sol and ml into full_sol, which assigns values to all variables in a. Using full_sol, it prints the values of each constraint term and the objective value, and also prints the resulting tours of the $V=3$ vehicles.

For example, the program prints the following results:

row_constraint = 0
column_constraint = 0
consecutive_constraint = 0
vehicle_constraint = 0
objective = 2142
Vehicle 0 : load = 91 / 100 : 0 -> 4(91) -> 0
Vehicle 1 : load = 177 / 200 : 0 -> 6(59) -> 5(66) -> 8(52) -> 0
Vehicle 2 : load = 288 / 300 : 0 -> 7(10) -> 9(83) -> 1(44) -> 3(94) -> 2(57) -> 0

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

A solution of the CVRP instance.


Last updated: 2026.02.02