vrplab 技術資料

このアプリが「何を決めて、何を最小化し、何を守っているか」と、 その解き方(アルゴリズム)をまとめます。

扱う問題

容量制約付き配送計画問題(CVRP)に時間枠(VRPTW)と、デポの開設判断を加えたものです。 デポは1つ以上置けますが、車両は所属デポから出て所属デポへ帰り、デポを跨いで移動しません。 車両とデポの組み合わせまで最適化する「真の複数デポVRP(MDVRP)」は扱いません。

一方でデポを開くか閉じるかはソルバが決めます。各デポに開設固定費があり、 「固定費を払ってこの拠点を開ける価値があるか」を輸送費と合わせて判断します。 車両の所属デポ自体は動かないので、上の MDVRP 除外と矛盾しません。

デポごとに車両を固定配置するイメージ デポAの車両0・1はデポA側の配送先だけを回り、デポBの車両2・3はデポB側だけを回る。車両がデポを越えて移動することはない。 デポA 車両0・1 デポB 車両2・3 車両はデポを越えて移動しない
デポは複数持てるが、車両は所属デポに固定される。デポAの車両0・1はデポA側だけ、 デポBの車両2・3はデポB側だけを回り、車両とデポの組み合わせまで最適化する 「真の複数デポVRP」は扱わない。

スコープ外: 車両種別ごとの仕様差、集荷と配送の混在(pickup & delivery)、 実道路の距離行列、CSV入出力、厳密解・最適性の保証。

決定変数(ソルバが決めるもの)

ソルバが探索するのは次の2つだけです。

変数取りうる値意味
割当 各配送先 → 車両番号 0..V−1 のどれか、または「未訪問」 どの車両が運ぶか。運ばない選択(未訪問)も正規の選択肢
訪問順 各車両に割り当てられた配送先の順列 どの順に回るか。デポは両端に暗黙に入る

V は全デポの車両台数の合計です。車両番号はデポを先頭から並べて 各デポの車両台数の分だけ連番を割ったもの(例: デポA が3台なら 0,1,2、続くデポB が2台なら 3,4)で、 車両番号の空間は常に全候補デポ分で固定します。

デポの開閉は独立した変数にしていません 「デポ d が開いている」は「d に属する車両のうち1台以上が空でない(+強制開設指定)」として 割当から導出します。開閉フラグを別に持つと正本が2つになり、「フラグは開いているのに 車両が空」という矛盾した状態を作れてしまうためです。デポの閉鎖は変数ではなく、 その車両を使わせないマスクとして表現します。
割当と訪問順のイメージ 配送先ごとにどの車両に入れるか(割当)と、その車両内での順番(訪問順)を決める。割り当てられない配送先は未訪問として残る。 車両0 1 2 3 車両1 1 2 未訪問 (正常な結果)
各配送先について「どの車両に割り当てるか(割当)」と「その車両内での訪問順」をソルバが決める。 割り当てられない配送先は未訪問として残り、これも正常な結果として扱う。

目的関数(最小化するもの)

すべて実額(円)で、1日あたりの運用コストとして足し合わせた総額を最小化します。

総コストを最小化 =
Σ開設デポ d 開設固定費(d) デポ開設固定費(円/拠点)
+ 車両単価 × (空でないルートの本数) 車両固定費(円/台)
+ 距離単価 × Σルート r 距離km(r) 距離変動費(円/km)
+ 稼働単価 × Σルート r 稼働分(r) 人件費(円/分)
+ 未訪問単価 × (未訪問の件数) 機会損失(円/件)
+ 遅延単価 × Σ 遅延分 遅延(遅刻可モードのときだけ)
用語定義
開設デポそのデポの車両が1台以上使われている、または強制開設が指定されている
距離km(r)デポ→訪問順→デポ の直線距離 × 迂回係数の総和
稼働分(r)帰着時刻 − 出発可能時刻。時間枠待ちの待機時間を含む
遅延分サービス開始が時間枠の終了時刻を超えた分(早着は違反ではない)

既定の単価は次の値です。いずれも設定パネルから変更できます。

項目既定値内訳の想定
デポ開設固定費40,000 円/拠点賃料・常駐人員・設備の按分
車両固定費8,000 円/台リース・保険・税
距離60 円/km燃料・タイヤ・整備
稼働時間50 円/分人件費(時給3,000円相当)
未訪問20,000 円/件機会損失・再配達
遅延300 円/分遅延ペナルティ
既定値は「実務の目安」から少し外して校正しています 実務どおりに車両固定費を15,000円/台に置くと総コストの過半を占めてしまい、 経路を改善した効果が数字に埋もれて「最適化しても何も変わらない」画面になります。 車両を8,000円に抑え、代わりに人件費を項として入れることで、 経路で動かせる部分(距離+稼働時間)が総額の約4割を占めるようにしています。 黙って都合のよい数字にするのではなく、校正したことをここに明記します。
コスト内訳の例(都心25件・車両5台) 総コスト127,100円のうち、デポ開設固定費40,000円、車両固定費40,000円、距離9,600円、稼働時間37,500円。経路の工夫で動かせる距離と稼働時間の合計は総額の約37%を占める。 総コスト 127,100円/日の例(都心25件・車両5台) 経路の工夫で動かせる部分 ≈ 37% デポ開設固定費 40,000円 車両固定費 40,000円 距離 9,600円 稼働時間 37,500円
既定値を校正する際に使った例(都心25件・車両5台・総距離160km・稼働750分)。 固定費(デポ+車両)が総額の6割超を占め、経路を工夫して動かせる部分(距離+稼働時間)は 約37%になる。

稼働分に待機時間を含めるので、時間枠を厳しくすると待機が増えて人件費が上がるという トレードオフも金額で見えます。

制約条件

常に守るもの(ハード制約)

制約式どこで守るか
積載容量 ルートの需要量合計 ≤ 積載容量の上限 ルート単位の判定(違反があれば手を採用しない)
稼働時間上限 稼働分 ≤ 稼働時間の上限 同上
帰着期限 帰着時刻 ≤ 帰着期限 同上
車両台数 ルート本数 = 全デポの車両台数の合計(固定) 構造で守る(ルート配列の長さそのもの)
強制閉鎖デポ そのデポの車両は使用不可 構造で守る(候補車両から除外して到達不能にする)
配送先の車両固定 固定した車両からは移動できない 各近傍が移動先を検査
開設デポ数の上下限 開設数の下限 ≤ 開設デポ数 ≤ 開設数の上限 解全体の判定(1ルートだけでは分からないため、候補解の採否で見る)
カバー範囲 担当デポから配送先までの距離 ≤ デポからの最大距離 ルート単位の判定。範囲外の配送先は未訪問になる
カバー範囲は「デポとの距離」で測ります 経路上の累積距離ではありません。「何かあったときに拠点から N km 以内で駆けつけられるか」 というサービスレベルの意味なので、実際にどう回ったかには依存しません。 コストを払えば破ってよいものではないので、遅刻可に切り替えても緩みません。 地図に描くカバー円の半径は「最大距離 ÷ 迂回係数」です(制約は迂回係数を掛けた距離で 判定しているため。ここを合わせないと「円の中なのに運べない」という嘘の絵になります)。

切り替えられるもの

制約扱い
時間枠(遅着) 厳守: 間に合わない訪問は挿入しない(結果として未訪問になる)/ 遅刻可: 遅刻を許し遅延単価でコストに計上する
時間枠(早着) 違反ではなく待機。時間枠の開始時刻まで待ってからサービスを開始し、待機時間は稼働時間に算入する
未訪問は「実行不可能」ではありません 入り切らなかった配送先は解の正常な一部として「未訪問」に残り、 地図上ではグレーのピンで表示され、目的関数にはペナルティとして入ります。 「時間枠を30分縮めたら5件回れなくなった」という一番見せたい現象を、 エラーではなく結果として出すための設計です。矛盾した設定(全デポを強制閉鎖した、 強制開設したデポ数が上限を超えた等)は計算前に検証してエラーにします。
時間枠の扱いのイメージ 早着は違反ではなく待機として扱う。遅着は、厳守モードでは挿入せず未訪問になり、遅刻可モードでは遅延コストを払って挿入する。 早着のケース 時間枠 到着(早着) 待機 サービス開始 遅着のケース 厳守モード 未訪問になる 遅刻可モード 遅延コストを計上
上: 早着は違反ではなく待機として扱い、時間枠の開始時刻からサービスを始める。 下: 時間枠を過ぎた到着は、厳守モードでは挿入されず未訪問になり、遅刻可モードでは 遅延コストを払って挿入される。

アルゴリズム

厳密解ではなくヒューリスティック(発見的解法)です。 「初期解を作る → 少しずつ改善する → 行き詰まったら一部を壊して作り直す」を 時間予算が尽きるまで繰り返します。

1. 距離行列の事前計算

全地点(デポ+配送先)間の距離と所要時間を先に計算して表にします。 局所探索は同じ2点間の距離を何度も引くので、都度2点間の距離を計算し直すと無駄になります。 60地点なら 66×66 程度で、計算は瞬時です。

距離km = 2点間の直線距離(a, b) × 迂回係数 既定 1.3
所要分 = 距離km ÷ 平均速度 × 60

実道路距離ではなく直線距離の近似です。この近似は画面上にも明記しています。

2. 初期解:並列挿入法(Solomon I1 の簡略版)

  1. 空のルートを車両台数分だけ用意する
  2. まだ割り当てていない配送先すべてについて、「どの車両のどの位置に挿し込むか」の 目的関数の増分を全通り計算する
  3. 増分が最小の(配送先, 車両, 位置)を1つ確定して挿入する
  4. 2〜3 を繰り返し、実行可能な挿入が無くなったら残りを未訪問にして終了
挿入位置の候補を比較するイメージ 新しい配送先を既存ルートのどこに挿入するか、直前・間・直後の3つの候補で目的関数の増分を比較し、最も増分が小さい候補を採用する。 候補1: 直前に挿入 +42km 不採用 候補2: 間に挿入 +18km ✓ 採用 候補3: 直後に挿入 +35km 不採用
新しい配送先を挿入する位置ごとに目的関数の増分(距離に加え、新しく車両やデポを使う 費用も含む)を計算し、最も安い候補(候補2)を採用する。

増分は距離だけでなく、その車両を新しく使い始める費用と、そのデポを新しく開く固定費を含めます。 距離だけで選ぶと、空いている車両(=未開設デポ)へ気軽に配ってしまい、 せっかく閉じたデポをすぐ開け直してしまいます。

最近傍法から始めないのは、最後に遠い地点だけが残って極端に悪い解になりやすく、 時間枠との相性も悪いためです。挿入法は最初からそれなりの解を出すので、 改善過程の再生が「ぐちゃぐちゃからきれいへ」ではなく「それなりから良いへ」になり、 実務の感覚に近くなります。

3. 局所探索:4種の近傍を「改善即採用」方式で回す

近傍操作効く場面
relocate(移動)1地点を別の位置(別ルート含む)へ移す車両間の偏りの是正
swap(交換)2地点を入れ替える容量が詰まっていて移動できないとき
2-opt1ルート内で辺を2本切って区間を反転する経路の交差の解消
2-opt*2ルート間で後半をまるごと交換するルート同士が絡んでいるとき
4種の近傍のイメージ relocateは1地点を別ルートへ移す。swapは2地点を入れ替える。2-optはルート内の交差を反転で解消する。2-opt*は2ルート間で後半を交換する。 ① relocate(移動) 1地点を別ルートへ移す ② swap(交換) ↔ 2地点を入れ替える ③ 2-opt 交差した辺(前) 反転後の辺 ④ 2-opt* ↔ 2ルート間で後半を交換
relocateは1地点を別ルートへ移し、swapは2地点を入れ替える。2-optはルート内で交差した 2本の辺を切って区間を反転し、2-opt*は2ルート間で後半をまるごと交換する。

改善が見つかったら即座に採用し、どの近傍でも改善が見つからなく なるまで繰り返します。あわせて、未訪問からの挿入試行も毎周行います (これが無いと「台数を増やしたのに未訪問が減らない」という説明しづらい挙動が出ます)。

速度のため、各近傍は触った1〜2ルートだけの部分和でコストを比較します(解全体を 毎回再計算しません)。ただしデポ開設固定費は「そのデポの全車両が空か」で決まるため、 1ルートだけ見ても判定できません。そこで降下の開始時にデポごとの「空でないルート数」を 数えておき、触るルートを除いた数と突き合わせることで、 他の車両がまだそのデポを使っているかを厳密に判定しています。

4. 摂動:ruin & recreate(壊して作り直す)

局所探索が行き詰まったら、解の一部を壊して作り直し、改善していれば乗り換えます。壊し方は3種類です。

3種類の破壊のイメージ 一様ランダム破壊は複数ルートから少数の配送先だけを抜き取る。デポを閉じる破壊は1つのデポに属する車両のルートを全部空にする。デポを開く破壊は、閉じているデポの近くにある配送先を引き剥がす。 一様ランダム(50%) 複数ルートから少数だけ抜く デポを閉じる(30%) 1デポの全ルートを空にする デポを開く(20%) 閉じたデポの近くから引き剥がす
左: ランダムに選んだ少数の配送先だけを複数ルートから抜き取る。 中央: 1つのデポを選び、そのデポに属する車両のルートを全部空にする(閉じる方向)。 右: 閉じているデポ(点線の四角)の近くにある配送先を引き剥がし、 入れ直すあいだだけ「このデポは開いている」として測る(開く方向)。
壊し方内容選ばれる確率
一様ランダム破壊 割当済みの配送先から 10〜30% をランダムに取り除く 50%
デポを閉じる方向 開いているデポを1つ選び、そのデポの車両のルートを全部空にする 30%(デポが2つ以上あるとき)
デポを開く方向 閉じているデポを1つ選び、そのデポから近い順に5件の配送先を引き剥がす 20%(デポが2つ以上あるとき)
デポ単位の破壊がないと、デポは永遠に閉じられません デポ開設固定費は「最後の1件が抜けるまで消えない」性質を持つため、 1件ずつ動かす近傍では途中の手がすべて悪化に見えて棄却されます。 実測でも、固定費90万円のデポが1万回以上の反復でまったく閉じられませんでした。 「この拠点をやめる」を1手として評価できるようにするのが、この破壊の役割です。
デポを壊した回は、そのデポへの再挿入を禁止します 再挿入は「実行可能なら入れる」動作なので、残すデポの容量が足りないと、 入り切らない1件が高価なデポへ押し戻されて即座に再開設されてしまいます (実測でこれが起きました)。壊した回だけそのデポを候補から外すことで、 「このデポを閉じた案」を確実に作って評価します。入り切らない分は未訪問として残り、 ペナルティ込みで採否を判定します。
閉じた分の仕事を「どこへ移すか」も毎回変えます 再挿入の手順は決定的なので、閉じるデポが同じなら結果も毎回同じになります。 移し先を指定しないと、貪欲な挿入が最初の1件の代価だけで移し先を決めてしまい、 その1案が悪ければ何千回繰り返しても同じ手が棄却され続けます。 実測では、候補3拠点・9地点・固定費2万円ずつの問題で、最良は拠点1単独の74,081円なのに、 移し先を指定しないと8,853反復・4秒かけても拠点2単独の77,706円(4.9%悪い)から 動けませんでした。残った候補から1つ選び、挿入のあいだだけ「そこは開いている」として 測ることで、1,500ms・3,332反復で最良に到達します。
開く方向も別に用意しないと、探索は「拠点は少ないほどよい」に偏ります 閉じたデポへ最初の1件を入れる代価には開設固定費が丸ごと乗るため、 貪欲な挿入は絶対にそれを選びません(固定費は後続の件数で薄まるのに、初手だけで判断してしまう)。 片方向だけだと開設数の下限を満たす解にすら到達できなくなります(実測で下限2が黙って破られました)。 そのため初期解の直後に、下限を満たすまではコストを見ずにデポを開けておき (下限はハード制約なので「開かない方が安い」と判断させてはいけません)、 予算内に満たせなかったときは黙って返さず違反として表示します。
開設数の下限は、局所探索の側でも見ないと守れませんでした 「開設数は解全体にかかる制約だから、候補解を採るかどうかの段階で見ればよい」と考えて 実装したところ、下限4を指定したのに3拠点の解が返る状態になりました。 原因は、下限を満たすようにデポを開けた直後に走る局所探索です。局所探索はコストしか見ないので、 開けたばかりのデポの最後の1件を他のデポへ移して、閉じてしまっていました (地点の移動とルート後半の交換の2つがこれを起こせます)。 そこで、下限を割ってしまう手はコストが下がっても採用しないようにしています。 ただし「下限を割って、かつ開設数を減らす手」だけを対象にします。 すでに下限を割っている状態で一切の手を止めると、そこから改善もできなくなるためです。

5. 時間予算と中断

打ち切りは反復回数ではなく実際に経過した時間で判定します。 既定の予算は500msで、画面から変更できます。局所探索の側も外側ループごとに 期限を見ます(60地点規模では1回の降下に200ms以上かかるため、ここを見ないと予算を超過します)。 各近傍は手を採用した時点で処理を終えるので、どこで打ち切っても解は壊れません。

画面側では世代番号を持ち、計算中に新しい要求が来たら Worker を作り直して古い結果は捨てます。 「中断は常に効く」ことを要件にしています。

6. 再現性

乱数は、シード値(種になる数)を固定した専用の生成器(xorshift32)だけを使い、 実行ごとに結果が変わる標準の乱数機能は使いません。 同じシナリオ・同じ設定・同じシード値なら、開設するデポの選択まで含めて常に同じ結果になります。 シード値は画面から変更できるので、「同じ問題でも初期条件が違えば違う解に落ちる」という ヒューリスティックの性質も見せられます。

7. デポの開け方を全比較する(任意)

通常の計算は「見つけた中で一番良かった1案」しか見せません。しかし 「拠点を1つ増やすと固定費はいくら増えて輸送費はいくら減るのか」を読むには、 採用されなかった開け方も並んでいる必要があります。 そこで、開け方(空の組み合わせを除く 2のデポ数乗 − 1 通り)をすべて数え上げて 表にするビューを、任意で開けるようにしています。

これは既定の解き方ではありません。既定はデポ単位の破壊近傍のままです。 また、全部を数え上げているのは開け方だけで、 それぞれの開け方の中の配送は近似のままなので、「これが最適」とは言えません。

段やること理由
1段目 すべての開け方を、局所探索1回だけで粗くふるいにかける 1つの開け方でも1降下に25〜241msかかるため、全部に本予算を配ると候補6拠点で数十秒になる
2段目 上位5件だけを、画面で設定した計算時間で解き直す 粗い見積りは絶対額がずれるので、少なくとも表の上位は確定値にしないと金額として読めない
時間を均等に配るのではなく、2段にする理由 実測(40地点・候補4拠点・全15通り)では、1降下の見積りは絶対額で最大33ずれ、 これは上位候補どうしの差(377 と 393 の差=16)を上回りました。 つまり均等配分では順位を付けるだけの分解能がありません。 一方で上位数件の集合は1降下でも本探索と一致したので、 「粗くふるってから上位だけ解き直す」が成立します。 候補6拠点・18地点の教材シナリオでは、63通りの比較が約2.9秒で終わります。
表の読み方で2つ注意があります 解き直した行には「確定」、1降下のままの行には「見積り」と表示します。 見積りの行は順位が入れ替わることがあります。
また打ち切りが時間制なので、金額は1円単位では再現しません (同じ設定でもパソコンの混み具合で反復回数が変わります)。表の金額は目安として読んでください。

表に並ぶのは、いまの設定の下で成立する開け方だけです。強制開設・強制閉鎖に反するもの、 開設数の上下限に反するもの、車両を固定した配送先のデポが閉じてしまうものは除き、 除いた件数と理由も画面に出します(黙って減らすと「全部見た」が嘘になります)。 候補が7拠点以上のときは、一部だけ見せるのではなくエラーにします。

各行の「この開け方にする」を押すと、その組み合わせを強制開設・強制閉鎖として設定し、 解き直します。このとき出る金額は表に出ていた金額と一致します (開け方ごとにシード値をずらさないようにしているためです。 ずらす実装にしていた時期があり、表の数字が再現しないという問題になりました)。

計算時間の目安

初期解の構築+局所探索を1回通したときの実測値です(摂動を含まない、1回の降下)。

配送先デポ車両初期解局所探索合計
251515ms20ms35ms
403935ms68ms103ms
603961ms139ms200ms
6061874ms158ms232ms
規模と計算時間の関係 配送先・デポ・車両の数が増えるほど計算時間は伸びるが、既定の時間予算500msには収まる。 既定の時間予算 500ms 35ms 25件・1デポ・5台 103ms 40件・3デポ・9台 200ms 60件・3デポ・9台 232ms 60件・6デポ・18台
上の表と同じ実測値(初期解構築+局所探索1回分)。規模が大きくなるほど時間は伸びるが、 いずれも既定の時間予算500ms(点線)に収まっている。

想定規模は10〜60地点です。既定の500msでも、25地点なら100回以上摂動を回せますが、 60地点・6デポでは1〜2回しか回りません。規模が大きいときは予算を増やしてください。

最適性について(厳密解ではありません)

得られる解は良い解ですが、最適であることの保証はありません。下界も出しません。 これは意図した割り切りです。

配送計画とデポの開設判断を同時に厳密に解く問題(Location-Routing Problem)は、 経路の変数が地点数の2乗のオーダーで増え、部分巡回路を除去する追加の仕組みも必要になるため、 想定規模の上限(60地点・複数デポ)では現実的な時間で最適性を示せません。 加えて厳密解ソルバは「一発で答えを出す」性質上、 改善過程の再生ができなくなります。「最適化が何をしているのか」を動きで見せることを 重視しているため、ヒューリスティックを選んでいます。

経路を無視してよい(施設から需要地点への距離だけで評価してよい)問題であれば、 姉妹アプリ flplab が厳密解を出します。経路まで見た近似が vrplab、 経路を無視した厳密解が flplab、という住み分けです。

ヒューリスティックと厳密解の違いのイメージ vrplabのヒューリスティックは使える時間の中で段階的にコストを下げ、最適値には触れないが十分近づく。厳密解は最適値に届くが、地点数が増えると現実的な時間では終わらない。 使える時間 (数百ms) 計算時間 → コスト(低いほど良い) 真の最適値 ヒューリスティック(vrplab) 厳密解
ヒューリスティック(vrplab)は使える時間(数百ms)の中で段階的に改善し、真の最適値には 触れないが十分近づく。厳密解は最適値に届くが、地点数が増えると現実的な時間では終わらない。

正しさの守り方

最も危険なのは評価関数(到着時刻の累積・待ち時間・時間枠違反・積載の累積・ デポの所属解決)です。ここを1箇所間違えても、ソルバはエラーを出さず 「一貫して間違った解」を返すため、地図はきれいに描かれ数字も出てしまい、 見た目では気づけません。次の4つで守っています。

  1. 手計算した期待値テスト — 3地点の例を手で解き、距離・到着時刻・待ち時間・ コストの期待値をテストに直書きする
  2. 総当たりとの突き合わせ — 5地点以下なら全割当・全順列を列挙して最良解を求め、 ソルバの解がそれより悪くないことを確認する
  3. 差分と全再計算の一致 — 局所探索が使う「部分和のデルタ」と、 解全体を再計算したときのコスト差が一致することを、ランダムな手で何通りも確認する。 ここがずれると、探索は嘘の改善を採用し続ける
  4. コスト内訳の合計一致 — 画面に出す内訳の合計が総コストと一致することを確認する
正しさを守る4段構えのイメージ 手計算した期待値テスト、総当たりとの突き合わせ、差分と全再計算の一致、コスト内訳の合計一致という4段の確認を順に通す。 1 手計算の 期待値テスト 2 総当たりとの 突き合わせ 3 差分と全再計算 の一致 4 コスト内訳の 合計一致
4段構えで守る。どれか1つが漏れても、他の段が矛盾を検出できるようにしている。
← アプリに戻る