容量制約付き配車計画問題(CVRP)

容量制約付き配車計画問題(CVRP)は、単一のデポから出発して戻る $V$ 台の車両の経路集合を求め、すべての顧客にサービスを提供することを目的とする問題です。 場所を $i \in \lbrace 0,1,\ldots,N-1\rbrace$ でインデックス付けし、場所0をデポ、場所 $1,\ldots,N-1$ を顧客とします。 各顧客 $i\in \lbrace 1,\ldots,N-1\rbrace$ には配送すべき需要量 $d_i$ があります(デポでは $d_0=0$ とします)。 各車両 $v \in \lbrace 0,\ldots,V-1\rbrace$ はデポを出発し、顧客の部分集合を訪問してデポに戻ります。このとき、経路上の配送需要の合計が車両容量 $q_v$ を超えないという容量制約を満たす必要があります。 目的関数は、全車両の総移動コストを最小化することです。

場所は2次元平面上の点であり、2つの場所間の移動コストはユークリッド距離であると仮定します。 $c_{i,j}$ を場所 $i$ と $j$ の間の距離(コスト)とします。

QUBO++定式化:バイナリ変数の配列

CVRPをQUBO式として定式化するために、$V\times N\times N$ のバイナリ変数配列 $A=(a_{v,t,i})$($0\leq v\leq V, 0\leq t,i\leq N-1$)を使用します。 ここで $a_{v,t,i}$ は、車両 $v$ の $t$ 番目に訪問する場所が場所 $i$ である場合にのみ1となります。

以下は $V=3$、$N=10$ における $A=(a_{v,t,i})$ の割り当ての例で、CVRPの解を表しています。 各車両はデポ(場所0)から出発し、いくつかの顧客を訪問した後デポに戻ります。 車両がデポに戻った後は、残りの時間ステップではデポに留まります。 各顧客 $1, \ldots, 9$ はちょうど1台の車両によってちょうど1回訪問されるため、この配列は実行可能なCVRPの解を表しています。

車両 $v=0$

巡回路: $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

車両 $v=1$

巡回路: $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

車両 $v=2$

巡回路: $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

QUBO++定式化の制約

行制約(各時刻でone-hot)

各車両は各時刻 $t$ でちょうど1つの場所にいなければなりません。 one-hot制約を課します:

\[\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 は、すべての行がone-hotである場合にのみ最小値 $0$ をとります。

また、最初の位置をデポに固定します:

\[\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}\]

これらは変数の固定により強制できます。

連続デポ制約(「デポを再び出発しない」制約)

車両がデポに戻った後に再び顧客を訪問するパターンを禁止します。 デポ指示変数 $a_{v,t,0}$ を用いて、列

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

が連続する0の後に連続する1で構成される(すなわち1になったら1のまま)ことを要求します。 禁止された遷移 $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 は、デポ指示変数が $t$($1\leq t\leq N-1$)について単調非減少である場合にのみ0をとります。

この制約は距離メトリックが三角不等式を満たす場合は冗長(そのような「再出発」解は最適になり得ない)ですが、ソルバーがそのような非最適な構造を避けるのに役立つことが多いです。

列制約

各顧客はちょうど1台の車両によってちょうど1つの時刻に1回だけ訪問されなければなりません:

\[\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 は、すべての顧客 $i = 1, \dots ,N-1$ がちょうど1回訪問される場合にのみ0をとります。

容量制約

各車両 $v$ の配送需要の合計は

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

であり、$q_v$ 以下でなければなりません。 以下の制約が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_ia_{v,t,i}\leq q_v\Bigl) \end{aligned}\]

capacity_constraint は、すべての車両が容量を超えない場合にのみ0をとります。

QUBO定式化の目的関数

総巡回コストは連続して訪問する場所から計算されます:

\[\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}a_{v,t,i}a_{v,(t+1)\bmod N, j} \end{aligned}\]

車両 $v$ が時間ステップ $t$ で場所 $i$ を訪問し、時間ステップ $(t+1)\bmod N$ で場所 $j$ に移動する場合、$a_{v,t,i}=a_{v,(t+1)\bmod N, j}=1$ となります。 この場合、対応する項は $c_{i,j}$ を和に寄与します。したがって、すべての制約が満たされている(各 $(v,t)$ にちょうど1つのアクティブな場所がある)とき、$\text{objective}$ はすべての車両の総移動コストに等しくなります。

ユークリッドメトリックの下では、すべての $i$ に対して $c_{i,i}=0$ であるため、同じ場所に留まっても追加コストは発生しません。

CVRPのQUBO定式化

目的関数と制約を組み合わせて、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}\]

ここで $P$ は十分に大きな定数であり、コスト最小化よりも実行可能性(制約充足)が優先されます。

PyQBPPプログラム

以下のPyQBPPプログラムは、$N=10$ 箇所、$V=3$ 台の車両のCVRPインスタンスの解を求めます。 リスト locations は3つ組 (x,y,d) を格納し、(x,y) は場所の2D座標、d は顧客の需要量(デポの需要量は0)です。 リスト vehicle_capacity は $V=3$ 台の車両の容量を格納します。 この例では [100, 200, 300] に設定されているため、車両0、1、2はそれぞれ小、中、大の容量を持ちます。

import math
import pyqbpp as qbpp

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)]
vehicle_capacity = qbpp.array([100, 200, 300])

N = len(locations)
V = len(vehicle_capacity)

a = qbpp.var("a", shape=(V, N, N))

row_constraint = qbpp.sum(qbpp.vector_sum(a) == 1)

column_sum = [0 for _ in range(N - 1)]
for v in range(V):
    for t in range(N):
        for i in range(1, N):
            column_sum[i - 1] += a[v][t][i]
column_constraint = 0
for i in range(N - 1):
    column_constraint += (column_sum[i] == 1)

consecutive_constraint = 0
for v in range(V):
    for t in range(1, N - 1):
        consecutive_constraint += a[v][t][0] * (1 - a[v][t + 1][0])

vehicle_load = [0 for _ in range(V)]
capacity_constraint = 0
for v in range(V):
    for t in range(N):
        for i in range(1, N):
            vehicle_load[v] += a[v][t][i] * locations[i][2]
    capacity_constraint += (0 <= vehicle_load[v]) & (qbpp.same <= vehicle_capacity[v])

objective = 0
for v in range(V):
    for t in range(N):
        next_t = (t + 1) % N
        for i in range(N):
            x1, y1 = locations[i][0], locations[i][1]
            for j in range(N):
                x2, y2 = locations[j][0], locations[j][1]
                dist = int(math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2))
                objective += dist * a[v][t][i] * a[v][next_t][j]

f = objective + 10000 * (row_constraint + column_constraint +
                          consecutive_constraint + capacity_constraint)

ml = {a[v][0][0]: 1 for v in range(V)}
ml.update({a[v][0][i]: 0 for v in range(V) for i in range(1, N)})

g = qbpp.replace(f, ml)
f.simplify_as_binary()
g.simplify_as_binary()
solver = qbpp.EasySolver(g)

sol = solver.search()

full_sol = qbpp.Sol(f).set(sol, ml)

print(f"row_constraint = {full_sol(row_constraint)}")
print(f"column_constraint = {full_sol(column_constraint)}")
print(f"consecutive_constraint = {full_sol(consecutive_constraint)}")
print(f"capacity_constraint = {full_sol(capacity_constraint)}")
print(f"objective = {full_sol(objective)}")

for v in range(V):
    load = full_sol(vehicle_load[v])
    route = f"Vehicle {v} : load = {load} / {vehicle_capacity[v]} : 0 "
    for t in range(1, N):
        for i in range(1, N):
            if full_sol(a[v][t][i]) == 1:
                route += f"-> {i}({locations[i][2]}) "
                break
    route += "-> 0"
    print(route)

このプログラムは、$V\times N\times N$ のバイナリ変数配列 a を定義します。 次に、上記の定式化に従って目的関数項 objective と制約項 row_constraintcolumn_constraintconsecutive_constraintcapacity_constraint を定義し、ペナルティ付き目的関数 f = objective + 10000 * ( row_constraint + column_constraint + consecutive_constraint + capacity_constraint ) にまとめます。

すべての車両の最初の訪問場所($t=0$)をデポ(場所0)に固定するために、プログラムは辞書 ml を構成し、qbpp.replace() を使って f に適用します。 結果の式は g に格納されます。 次に Easy Solver を使って g を最小化する割り当て sol を探索します。

g には ml で固定された変数が含まれないため、プログラムは solmlfull_sol にマージし、a のすべての変数に値を割り当てます。 full_sol を使って各制約項と目的関数の値を出力し、$V=3$ 台の車両の巡回路も出力します。

例えば、プログラムは以下の結果を出力します:

row_constraint = 0
column_constraint = 0
consecutive_constraint = 0
capacity_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

matplotlibによる可視化

以下のコードはCVRPの解を可視化します:

import matplotlib.pyplot as plt

vehicle_colors = ["#e74c3c", "#3498db", "#2ecc71"]
plt.figure(figsize=(6, 6))
for i, (lx, ly, q) in enumerate(locations):
    plt.plot(lx, ly, "ko", markersize=8)
    plt.annotate(f"{i}" + (f"({q})" if q > 0 else ""),
                 (lx, ly), textcoords="offset points", xytext=(5, 5))

for v in range(V):
    route_nodes = [0]
    for t in range(1, N):
        for i in range(1, N):
            if full_sol(a[v][t][i]) == 1:
                route_nodes.append(i)
                break
    route_nodes.append(0)
    for k in range(len(route_nodes) - 1):
        fr, to = route_nodes[k], route_nodes[k + 1]
        plt.annotate("", xy=(locations[to][0], locations[to][1]),
                     xytext=(locations[fr][0], locations[fr][1]),
                     arrowprops=dict(arrowstyle="->", color=vehicle_colors[v],
                                    lw=2))
plt.title("CVRP Solution")
plt.savefig("cvrp.png", dpi=150, bbox_inches="tight")
plt.show()

各車両の経路は異なる色で表示され、矢印が移動方向を示しています。


Back to top

Page last modified: 2026.05.21.