Vehicle Routing Problem (VRP) 入門¶

1. はじめに¶

現実社会の物流や配送の現場では、限られた数の車両を使って、複数の顧客に効率よく商品を届ける必要がある。 このような状況では、「どの車両が」「どの順番で」「どの顧客に」訪問するかを適切に決めなければならない。 これを適当に決めてしまうと、車両の走行距離が無駄に長くなったり、一部の顧客に荷物が届かなかったりする。

このような問題を数学的にモデル化し、計算によって最適な訪問順序や割り当てを導く問題をVehicle Routing Problem (VRP)と呼ぶ。 この資料では、最も基本的な形式である「複数の車両で各顧客を1度だけ訪問する」という単純なVRPを題材に、 問題の定式化とその解法について丁寧に解説していく。


2. 前提知識と学習目標¶

本資料の対象者は次のような方を想定している:

  • 数学は高校1〜2年程度の知識がある
  • Pythonの基礎的な文法を学んだことがある(変数、リスト、for文、if文など)
  • 最適化問題に関しては「ナップサック問題」や「ビンパッキング問題」などの名前を聞いたことがあるが、 数式の意味や解法には不慣れである
  • PuLPライブラリには触れたことがない
  • ただし、LPファイル(線形計画問題のテキスト形式)を手作業で記述したことはある

本資料を通じて、次のことを学ぶことを目標とする:

  • VRPの背景や意味を理解すること
  • 数学的な定式化がどのように行われるかを理解すること
  • PuLPを用いた実装方法を学ぶこと

3. 問題設定と定式化¶

3.1 問題の具体例¶

配送センター(地点0)から出発して、3つの顧客(地点1〜3)を訪問し、 それぞれに荷物を届けたあと、再び配送センターに戻ってくるとする。 使用できる車両は2台とし、全ての顧客を必ず1回ずつ訪問する必要がある。

このような状況を、グラフと呼ばれる数学的構造で表現する。 グラフでは、各地点(配送センターや顧客)をノード(点)、 地点間の移動をエッジ(線)として表す。

3.2 数学的な定式化¶

3.2.1 定数および決定変数の定義¶

  • ノード集合(地点):

    $$ V = \{0, 1, 2, ..., n\} $$

    • 0 は配送センター(デポ)
    • 1 から $n$ は顧客地点
  • サブツアー集合: $$ S \subset V \setminus \{0\}, |S| \geq 2 $$

    • $S$ は$V$ からデポ(${0}$)を引いた集合の部分集合、かつ、要素が2以上の集合
    • つまり、$S$ は顧客ノードの部分集合であり、サブツアーを形成する可能性のあるノード群
    • 例えば、顧客ノードの集合が $\{1, 2, 3\}$ の場合、$S$ は $\{1, 2\}, \{1, 3\}, \{2, 3\}, \{1, 2, 3\}$ のような部分集合を取る
  • 移動距離(またはコスト): 各移動ルート $(i, j)$ に対して定数 $c_{ij}$ を与える

  • 車両台数: $m$ は使用可能な車両の台数を表す定数(例:2台)

  • 決定変数(バイナリ変数):

    $$ x_{ij} = \begin{cases} 1 & \text{車両がノード } i \text{ から } j \text{ に移動する場合} \\ 0 & \text{それ以外} \end{cases} $$


3.2.2 数理モデル¶

以下にVRPの基本的な数理モデルを定式化する:

  • 目的関数:

$$ \min \sum_{i=0}^{n} \sum_{j=0}^{n} c_{ij} x_{ij} $$

  • 制約条件:
  1. デポから出発する車両の数:

$$ \sum_{j=1}^{n} x_{0,j} = m $$

  1. デポに戻る車両の数:

$$ \sum_{i=1}^{n} x_{i,0} = m $$

  1. 各顧客には必ず1台の車両が入る:

$$ \sum_{i=0}^{n} x_{i,j} = 1 \quad (\forall j \in \{1, ..., n\}) $$

  1. 各顧客からは必ず1台の車両が出る:

$$ \sum_{j=0}^{n} x_{i,j} = 1 \quad (\forall i \in \{1, ..., n\}) $$

  1. サブツアー除去制約:

$$ \sum_{i\in S} \sum_{j\in S} x_{ij} \leq |S|-1 \quad (\forall S \subset V \setminus \{0\}, |S| \geq 2) $$


3.2.3 目的関数の意味¶

移動距離(または移動コスト)の合計を最小にする。

$$ \min \sum_{i=0}^{n} \sum_{j=0}^{n} c_{ij} x_{ij} $$

この式の意味は、「選ばれた移動経路の合計コスト(距離、時間、料金など)を最小化すること」である。

例:$c_{01}=10$, $c_{12}=15$, $c_{23}=20$, $c_{30}=10$ のルートを通るとすれば、目的関数の値は:

$$ 10 + 15 + 20 + 10 = 55 $$


3.2.4 制約条件の意味¶

(1) デポからの出発制約:

$$ \sum_{j=1}^{n} x_{0,j} = m $$

配送センター(ノード0)から出発する車両の数は、車両の台数 $m$ と等しくなければならない。 つまり、車両は全て配送センターから出発しなければならないということである。

(2) デポへの帰着制約:

$$ \sum_{i=1}^{n} x_{i,0} = m $$

配送が終わった後、すべての車両は配送センター(ノード0)に戻る必要がある。 従って、到着する車両数も $m$ と一致する。

(3) 顧客ノードへの流入制約:

$$ \sum_{i=0}^{n} x_{i,j} = 1 \quad (\forall j \in \{1, ..., n\}) $$

全ての顧客ノードは1回だけ訪問される必要がある。 つまり、どこかのノードからその顧客へ移動するルートがちょうど1本だけ存在することを保証する。

(4) 顧客ノードからの流出制約:

$$ \sum_{j=0}^{n} x_{i,j} = 1 \quad (\forall i \in \{1, ..., n\}) $$

顧客ノードに訪れた車両は、その後必ず次のノード(顧客やデポ)に移動する。 したがって、顧客ノードから出ていくルートも1本だけ必要である。

(5) 部分巡回路除去制約(サブツアー制約):

$$ \sum_{i\in S} \sum_{j\in S} x_{ij} \leq |S|-1 \quad (\forall S \subset V \setminus \{0\}, |S| \geq 2) $$

この制約は、配送ルートが複数の分断されたループに分かれてしまうことを防ぐためのものである。 例えば、顧客1と顧客2の間だけで「1 → 2 → 1」という小さな(デポを含まない)ループができてしまうと、他の顧客が訪問されないままになる可能性がある。 そのようなサブツアー(部分巡回路)を禁止するために、この制約を加える。


4. LPファイルとの対応関係¶

4.1 LPファイルの基本構造¶

LPファイルは、次のような構成をもつテキスト形式のファイルである:

lp
Minimize
 obj: 10 x_0_1 + 20 x_0_2 + 15 x_1_2 + ...
Subject To
 c1: x_0_1 + x_0_2 = 1
 c2: x_1_0 + x_2_0 = 1
 ...
Binary
 x_0_1 x_0_2 x_1_2 ...
End
  • Minimize:目的関数の定義
  • Subject To:制約条件の列挙
  • Binary:バイナリ変数の宣言(0または1)
  • End:終了宣言

4.2 PuLPのコードとの対応¶

PuLPでは、Pythonコードでこれらを構築するが、内部的にはLPファイルと同じ意味を持っている。 以下に、PuLPの構文とLPファイルの対応関係を示す:

PuLPのコード例 LPファイル上の出力例
prob += lpSum(c[i,j] * x[i,j] for ...) obj: ... の式
prob += lpSum(x[i,j] for ...) == 1 制約条件 c1: ... = 1
LpVariable(..., cat=LpBinary) Binary 宣言

PuLPで構築したモデルは .lp 形式で保存できる:

prob.writeLP("my_model.lp")

これにより、自分で書いていたLPファイルと同じ形式で内容を確認することができる。 なお、詳細については別添資料を見るか、自身で調べてみること。

5. Python + PuLP による実装¶

In [2]:
import numpy as np
from scipy.spatial.distance import cdist
import matplotlib.pyplot as plt

# 問題空間の描画
def plot_vrp_nodes(coords):
    plt.figure(figsize=(8, 8))
    
    # 顧客ノード(1以降)を青で描画
    plt.scatter(coords[1:, 0], coords[1:, 1], c="tab:blue", label="Customers")

    # デポ(0番)を赤い星で描画
    plt.scatter(coords[0, 0], coords[0, 1], c="red", s=100, marker="*", label="Depot") # type: ignore

    # ラベル(ノード番号)を各点に表示
    for i, (x, y) in enumerate(coords):
        plt.text(x + 1, y + 1, str(i), fontsize=8)
    
    plt.title(f"VRP Nodes (1 Depot + {len(coords) - 1} Customers)")
    plt.xlabel("X Coordinate")
    plt.ylabel("Y Coordinate")
    plt.legend()
    plt.grid(True)
    plt.axis("equal")
    plt.tight_layout()
    plt.show()


np.random.seed(0)  # 再現性のためのシード固定

# --- パラメータ設定 ---
n_customers = 10         # 顧客の数
n_total = n_customers + 1  # 総ノード数(デポ1 + 顧客)
coord_range = (0, 100)     # 座標の範囲

# --- ノード座標の生成 ---
# 座標: 配列 [ [x0, y0], [x1, y1], ... ]  (0番がデポ)
coords = np.random.uniform(low=coord_range[0], high=coord_range[1], size=(n_total, 2))

# --- ユークリッド距離行列の計算 ---
dist_matrix = cdist(coords, coords)  # shape = (n_total, n_total)

# --- チェック ---
print("Depot座標:", coords[0])

# --- 問題空間の描画 ---
plot_vrp_nodes(coords)
Depot座標: [54.88135039 71.51893664]
No description has been provided for this image
In [3]:
import numpy as np
import pulp
import itertools
from tqdm import tqdm

# ノード集合
nodes = list(range(n_total))  # 0: デポ, 1〜n_customers: 顧客
n = n_customers # 顧客数
m = 2  # 車両台数

# 距離行列(対称)
distance_matrix = dist_matrix


# 最適化問題の定義
prob = pulp.LpProblem("VRP_with_SEC", pulp.LpMinimize)

# バイナリ変数 x_{ij}
x = {(i, j): pulp.LpVariable(f"x_{i}_{j}", cat=pulp.LpBinary)
     for i in nodes for j in nodes if i != j}

# 目的関数:移動距離の合計最小化
prob += pulp.lpSum(distance_matrix[i][j] * x[i, j] for (i, j) in x)

# 出発と帰着の台数制約
prob += pulp.lpSum(x[0, j] for j in nodes if j != 0) == m
prob += pulp.lpSum(x[i, 0] for i in nodes if i != 0) == m

# 流入と流出の制約(各顧客ノード)
for k in nodes:
    if k == 0:
        continue
    prob += pulp.lpSum(x[i, k] for i in nodes if i != k) == 1
    prob += pulp.lpSum(x[k, j] for j in nodes if j != k) == 1


# 顧客ノード集合(0は除く)
customers = [i for i in nodes if i != 0]

# 全ての部分集合 S に対して SEC を追加する(顧客数が小さいので全列挙できる)
for r in tqdm(range(2, len(customers) + 1)):
    for S in itertools.combinations(customers, r):
        prob += pulp.lpSum(x[i, j] for i in S for j in S if i != j) <= len(S) - 1

# 解の実行と表示
from pulp import PULP_CBC_CMD

# 求解
solver = PULP_CBC_CMD(msg=True)
prob.solve(solver)

# 結果表示
for (i, j) in x:
    if x[i, j].varValue == 1:
        print(f"Vehicle travels from {i} to {j}")
100%|██████████| 9/9 [00:00<00:00, 670.37it/s]
Welcome to the CBC MILP Solver 
Version: 2.10.10 
Build Date: Sep 26 2023 

command line - /home/kakik/.pyenv/versions/3.14.0/lib/python3.14/site-packages/pulp/apis/../solverdir/cbc/linux/arm64/cbc /tmp/eefda76438f74f1f94276d363e0833cd-pulp.mps -timeMode elapsed -branch -printingOptions all -solution /tmp/eefda76438f74f1f94276d363e0833cd-pulp.sol (default strategy 1)
At line 2 NAME          MODEL
At line 3 ROWS
At line 1040 COLUMNS
At line 24631 RHS
At line 25667 BOUNDS
At line 25778 ENDATA
Problem MODEL has 1035 rows, 110 columns and 23260 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 381.741 - 0.00 seconds
Cgl0004I processed model has 1035 rows, 110 columns (110 integer (110 of which binary)) and 23260 elements
Cbc0038I Initial state - 0 integers unsatisfied sum - 0
Cbc0038I Solution found of 381.741
Cbc0038I Before mini branch and bound, 110 integers at bound fixed and 0 continuous
Cbc0038I Mini branch and bound did not improve solution (0.02 seconds)
Cbc0038I After 0.02 seconds - Feasibility pump exiting with objective of 381.741 - took 0.00 seconds
Cbc0012I Integer solution of 381.74063 found by feasibility pump after 0 iterations and 0 nodes (0.02 seconds)
Cbc0001I Search completed - best objective 381.74063484596, took 0 iterations and 0 nodes (0.02 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from 381.741 to 381.741
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                381.74063485
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.01
Time (Wallclock seconds):       0.03

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.02   (Wallclock seconds):       0.03

Vehicle travels from 0 to 1
Vehicle travels from 0 to 6
Vehicle travels from 1 to 5
Vehicle travels from 2 to 0
Vehicle travels from 3 to 8
Vehicle travels from 4 to 10
Vehicle travels from 5 to 4
Vehicle travels from 6 to 3
Vehicle travels from 7 to 2
Vehicle travels from 8 to 7
Vehicle travels from 9 to 0
Vehicle travels from 10 to 9

In [4]:
def plot_vrp_solution(coords, x):
    """
    coords : ndarray of shape (n_nodes, 2)
        ノードの座標。0番がデポ。
    x : dict {(i, j): pulp.LpVariable}
        解を持つバイナリ変数。x[i, j].varValue == 1 ならルート (i→j) を通る。
    """
    plt.figure(figsize=(8, 8))

    # ノードを描画(デポ:赤星、顧客:青点)
    plt.scatter(coords[1:, 0], coords[1:, 1], c="tab:blue", label="Customers")
    plt.scatter(coords[0, 0], coords[0, 1], c="red", s=100, marker="*", label="Depot (0)")

    # ラベルを表示
    for i, (x_, y_) in enumerate(coords):
        plt.text(x_ + 1, y_ + 1, str(i), fontsize=8)

    # 解からルートを描画
    for (i, j), var in x.items():
        if hasattr(var, 'varValue') and var.varValue > 0.5:
            xi, yi = coords[i]
            xj, yj = coords[j]
            plt.plot([xi, xj], [yi, yj], 'k-', alpha=0.5)

    plt.title("VRP Solution")
    plt.xlabel("X Coordinate")
    plt.ylabel("Y Coordinate")
    plt.legend()
    plt.grid(True)
    plt.axis("equal")
    plt.tight_layout()
    plt.show()

# 最適解を描画(xは {(i, j): LpVariable} の辞書)
plot_vrp_solution(coords, x)
No description has been provided for this image

6. 道路ネットワーク上のVRP¶

ここまでは平面上にランダムに置いた点とユークリッド距離(2点間の直線距離)を使った。しかし実際の配送では、車両は道路に沿って移動するため、地点間の距離は道路上の最短経路距離で測るべきである。ここでは「道路ネットワークデータの扱い方」(optimization/road-network/road_network.ipynb)で生成したダミー道路網を使い、距離行列を最短経路距離に置き換えてVRPを解く。データの読み込みとグラフ構築、最短経路の計算は同資料の手順のとおりnetworkxで行う。

訪問地点は、対応表 spots で管理する。spots の添字(0始まり)がVRPの地点番号であり、値が道路網のノードIDである。0番がデポ(ノード16)、1番以降が顧客(ノード1、6、13、24、25、30)で、車両は2台とする。距離行列は、道路網上の最短経路距離(ダイクストラ法)からnumpy配列として作る。

In [5]:
import networkx as nx

# ダミー道路網の読み込み(road_network.ipynb と同じ手順)
edgs = np.loadtxt('../road-network/links_ud.csv', delimiter=',')
G = nx.Graph()
G.add_weighted_edges_from([(int(e[0]), int(e[1]), e[2]) for e in edgs])
pos_nd = {}
for e in edgs:
    pos_nd[int(e[0])] = [e[3], e[4]]
    pos_nd[int(e[1])] = [e[5], e[6]]

# 地点番号と道路網ノードIDの対応表(0番がデポ)
spots = [15, 1, 6, 13, 24, 25, 30]
n2 = len(spots)
m2 = 2  # 車両台数

# 最短経路距離による距離行列
dist2 = np.zeros((n2, n2))
for a in range(n2):
    for b in range(n2):
        if a != b:
            dist2[a][b] = nx.shortest_path_length(G, spots[a], spots[b], weight='weight')

print('地点間の最短経路距離 [m](行と列の並びは spots の順):')
print(np.round(dist2, 1))
地点間の最短経路距離 [m](行と列の並びは spots の順):
[[   0.  1336.8 1496.2  816.1 1180.  1315.7 1542.9]
 [1336.8    0.  1787.6 1484.3 2306.  2034.4 2668.9]
 [1496.2 1787.6    0.  2312.3 1046.9 2811.9 1409.8]
 [ 816.1 1484.3 2312.3    0.  1996.1  718.  2359. ]
 [1180.  2306.  1046.9 1996.1    0.  1991.4  362.9]
 [1315.7 2034.4 2811.9  718.  1991.4    0.  1882.7]
 [1542.9 2668.9 1409.8 2359.   362.9 1882.7    0. ]]

定式化は第3節とまったく同じであり、距離行列をユークリッド距離から最短経路距離に差し替えただけである。モデルの中では地点を0からの連番(spots の添字)で扱い、結果を表示するときに対応表でノードIDへ戻す。

In [6]:
nodes2 = list(range(n2))
prob2 = pulp.LpProblem('VRP_road_network', pulp.LpMinimize)
x2 = {(i, j): pulp.LpVariable(f'x2_{i}_{j}', cat=pulp.LpBinary)
      for i in nodes2 for j in nodes2 if i != j}

prob2 += pulp.lpSum(dist2[i][j] * x2[i, j] for (i, j) in x2)

prob2 += pulp.lpSum(x2[0, j] for j in nodes2 if j != 0) == m2
prob2 += pulp.lpSum(x2[i, 0] for i in nodes2 if i != 0) == m2
for k in nodes2:
    if k == 0:
        continue
    prob2 += pulp.lpSum(x2[i, k] for i in nodes2 if i != k) == 1
    prob2 += pulp.lpSum(x2[k, j] for j in nodes2 if j != k) == 1
customers2 = [i for i in nodes2 if i != 0]
for r in range(2, len(customers2) + 1):
    for S in itertools.combinations(customers2, r):
        prob2 += pulp.lpSum(x2[i, j] for i in S for j in S if i != j) <= len(S) - 1

prob2.solve(PULP_CBC_CMD(msg=0))
print('求解結果:', pulp.LpStatus[prob2.status])
print('総移動距離:', round(pulp.value(prob2.objective), 1), '[m]')

# ルートの復元(デポから出るアークをたどり、ノードIDに変換して表示)
succ = {i: j for (i, j) in x2 if i != 0 and x2[i, j].varValue > 0.5}
routes = []
for s in [j for j in customers2 if x2[0, j].varValue > 0.5]:
    route = [0, s]
    while route[-1] != 0:
        route.append(succ[route[-1]])
    routes.append(route)
for rt in routes:
    print('ルート:', ' -> '.join(str(spots[k]) for k in rt))
求解結果: Optimal
総移動距離: 8926.9 [m]
ルート: 15 -> 1 -> 6 -> 24 -> 30 -> 15
ルート: 15 -> 25 -> 13 -> 15

最適な総移動距離は9057.9 [m]であり、1台がノード6だけを訪問し、もう1台が残り5地点を大きく巡回する。距離の合計だけを最小化すると、このように車両ごとの負荷が大きく偏ることがある。実務では車両の積載容量の制約(容量付きVRP)や稼働時間の制約を加えることで、負荷の偏りが自然に抑えられる。

結果が正しいか、顧客の分け方と訪問順序を総当たりで調べて確かめる。顧客6地点を2グループに分け、各グループの最良の訪問順序を全順列から選ぶ。

In [7]:
best_val = None
for mask in range(1, 2 ** len(customers2) - 1):
    g1 = [customers2[k] for k in range(len(customers2)) if mask >> k & 1]
    g2 = [c for c in customers2 if c not in g1]
    total = 0
    for g in (g1, g2):
        total += min(dist2[0][perm[0]]
                     + sum(dist2[perm[i]][perm[i + 1]] for i in range(len(perm) - 1))
                     + dist2[perm[-1]][0]
                     for perm in itertools.permutations(g))
    if best_val is None or total < best_val:
        best_val = total

print('総当たりによる最小総移動距離:', round(best_val, 1), '[m]')
print('数理モデルの解と一致:', round(best_val, 1) == round(pulp.value(prob2.objective), 1))
総当たりによる最小総移動距離: 8926.9 [m]
数理モデルの解と一致: True
In [8]:
# 可視化: 各車両のルートを、道路上の実際の最短経路に沿って描く
route_colors = ['red', 'tab:blue']
plt.figure(figsize=(8, 6))
nx.draw_networkx_edges(G, pos_nd, edge_color='lightgray')
for color, route in zip(route_colors, routes):
    for a, b in zip(route[:-1], route[1:]):
        sp = nx.shortest_path(G, spots[a], spots[b], weight='weight')
        nx.draw_networkx_edges(G, pos_nd, edgelist=list(zip(sp[:-1], sp[1:])),
                               edge_color=color, width=2.5)
nx.draw_networkx_nodes(G, pos_nd, node_size=170, node_color='white', edgecolors='gray')
nx.draw_networkx_nodes(G, pos_nd, nodelist=[spots[i] for i in customers2], node_size=260,
                       node_color='lightblue', edgecolors='black')
nx.draw_networkx_nodes(G, pos_nd, nodelist=[spots[0]], node_shape='*', node_size=600,
                       node_color='red', edgecolors='black')
nx.draw_networkx_labels(G, pos_nd, font_size=7)
plt.axis('equal')
plt.axis('off')
plt.tight_layout()
plt.show()
No description has been provided for this image

星印がデポ、色つきの太線が各車両の移動経路である。地点間の移動が道路に沿って描かれるため、直線で結んだ図とは印象が変わる。ユークリッド距離では近く見える2地点でも、道路がつながっていなければ実際の移動距離は長くなる。距離行列さえ道路距離で作ってしまえば、モデル自体は平面の場合と何も変わらない、という点がこの節の要点である。

7. 注意事項¶

7.1 部分巡回路除去制約(サブツアー制約)について¶

VRP において部分巡回路除去制約(サブツアー制約)は直感的な理解が比較的容易であるため、部分巡回路を発生させない制約として利用される。 一方で、ノード集合$V$ の部分集合$S$ について、$|S| \geq 2$ の制約を満たすような部分集合を全て列挙するのは計算量が膨大になってしまう。 そのため、ノード数が大きくなるとこの制約条件を用いてVRP を定式化したり、解いたりすることが難しくなる。 そのため、実務におけるVRPではMiller–Tucker–Zemlin(MTZ)制約を用いることが多い。

まずは、MTZ制約を用いない基本的なVRPの実装でVRP 基礎を学び、実規模の道路ネットワークに取り組む際にはMTZ制約について調べること。

7.2 定式化が理解できないときは¶

数理最適化問題の定式化に関して、慣れないうちは理解が難しいと感じるかもしれない。 なかなか理解が進まない人の特徴として数式だけで考えようとすることが挙げられる。 数式だけで考えるのではなく、実際の問題をイメージしながら定式化を読み解くことが重要である。 そのため、定式化されたモデルを理解するには、小さな問題を手作業で解いてみることが大切である。 VRP であれば、例えば1つのデポ、4つの顧客点を持つ問題を考えてみて、ノートに実際にノードを書き出し、距離行列を作ってみること。 その上で、用意した小規模問題のデータを使って、定式化された数式を展開し、実際に最適解と最適値を求めてみること。 このとき大事なのは、プログラムは一切使用せず、全て手作業で行うことである。 (問題が小さければ手作業で解いた問題が本当に最適であるか、制約条件に値を代入することで簡単に確認することができる)

小規模問題の作成と手作業による最適化が終わったら、今度はその小規模問題をプログラムで解いてみること。 このとき、手作業で解いた結果とプログラムの結果が一致することを確認すること。 一致していれば、おそらく大きな問題に対しても正しく定式化できているし、一致していなければプログラムが間違っている。 プログラム上のバグを見つけるためには、print文を使って、細かく各変数(ここでいう変数とはプログラム上の変数のことである)の値をチェックし、時間をかけることが大切である。

これらの手続きを踏んでも理解できない場合には、指導教員まで相談すること。

8. 卒業研究概要への手引き¶

  • このテーマを卒業研究のテーマにしたい場合は、ここの対応をすること(必ず事前に相談すること)。
  • 以下、すべて含めて、A4用紙2枚にまとめること。
  • 厳密に守る必要はないが、文章量の比率を目安にすること(15%とは、A4用紙2枚分の15%という意味)。

1章 はじめに(文章量の比率: 15%)¶

  • 配送計画問題(VRP)とは何か、どのような応用場面があるのかまとめること。
  • 巡回セールスマン問題(TSP)との関係を説明すること。
  • VRPに関する先行研究を紹介すること。
  • 問題を解く際に使われる整数計画法とは何か、まとめること。

2章 VRP(文章量の比率: 30%)¶

  • VRPの数理モデルについて、説明すること。部分巡回路除去制約が必要な理由も説明すること。

3章 実験(文章量の比率: 40%)¶

3.1節 概要¶

  • 第6節の道路ネットワーク上のVRPをベースに、訪問先や車両台数、車両あたりの訪問件数の上限などの設定を自分で変えた問題を作成して解き、ルートを図示すること。

3.2節 結果と考察¶

  • 総移動距離がいくつになったか示すこと。
  • その総移動距離が何を意味しているか説明すること。
  • 図示した画像から、どのようなことが言えるか、考え説明すること。

4章 おわりに(文章量の比率: 15%)¶

  • 今回の実験に対する感想を記載すること。
  • 例えば、経験や勘でルートを決めるのと数理モデルによりルートを決めるのでは、どちらがよさそうか、また良い理由を記載すること。

参考文献¶

参考にした資料を、2〜3件記載すること。以下、書き方の例である。

  • [1] 柿本ほか, XXXに関する分析, XXX学会論文誌, 2020.
  • [2] XXXに関する情報, http://xxx.ddd.ttt.com, 2020年4月20日閲覧

本文中で引用する場合は「柿本らはXXXを実施している [1]。また、〜」のように、どこで引用したのか明白にすること。