E - 配送計画 / Delivery Plan Editorial by admin Editorial for printing
claude4.8opus-high概要
地点 \(1\) を出発し、指定された \(K\) 個の届け先をすべて経由して地点 \(N\) に到達するまでの最小移動時間を求める問題です。\(K \leq 15\) という小ささに注目し、最短経路 + 巡回セールスマン問題(bitDP) で解きます。
考察
まず重要な気づきは、「どの届け先をどの順番で回るか」だけが本質的な問題であるという点です。
届け先以外の地点は自由に通過してよいので、ある地点からある地点へ移動するときは、その \(2\) 地点間の最短経路を使えば最適です。つまり、
- 出発地 \(1\)
- 各届け先 \(C_1, C_2, \dots, C_K\)
- 到着地 \(N\)
これら「重要な地点」の間の最短距離さえ分かれば、あとは「どの順番で届け先を訪れるか」を決める問題に帰着できます。
ここで素朴に「届け先の訪問順を全部試す」と \(K!\) 通りの順列を調べることになります。\(K = 15\) では \(15! \approx 1.3 \times 10^{12}\) となり、到底間に合いません。
そこで、訪問順を全列挙する代わりに、「すでに訪れた届け先の集合」と「今いる届け先」 を状態として持つビット DP(巡回セールスマン問題の定石)を使います。これにより計算量を \(O(2^K \cdot K^2)\) まで落とせます。
アルゴリズム
ステップ 1: 重要地点間の最短距離を求める
辺の重みが正なので、ダイクストラ法を使います。
- 地点 \(1\) からの最短距離
distFrom1 - 各届け先 \(C_i\) からの最短距離
distC[i]
これらを求めておけば、
- 「\(1\) から \(C_i\) までの距離」=
distFrom1[C[i]] - 「\(C_i\) から \(C_j\) までの距離」=
distC[i][C[j]] - 「\(C_i\) から \(N\) までの距離」=
distC[i][N]
がすべて取り出せます。ダイクストラを実行する回数は \(1 + K\) 回です。
ステップ 2: ビット DP(巡回セールスマン)
dp[mask][i] を次のように定義します。
dp[mask][i]= 地点 \(1\) を出発し、maskで表される集合の届け先をすべて訪問し、最後に届け先 \(i\) にいる ときの最小移動時間
初期化: 最初に届け先 \(i\) を訪れる場合 $\( dp[\,2^i\,][i] = \text{distFrom1}[C_i] \)$
遷移: 集合 mask で最後に \(i\) にいる状態から、まだ訪れていない \(j\) へ移動する
$\(
dp[\text{mask} \mid 2^j][j] \leftarrow dp[\text{mask}][i] + \text{distC}[i][C_j]
\)$
答え: すべての届け先を訪れた状態(full = 全ビットが立った集合)から、最後の届け先 \(i\) を経由して \(N\) へ向かう最小値
$\(
\text{ans} = \min_{i} \left( dp[\text{full}][i] + \text{distC}[i][N] \right)
\)$
計算量
ダイクストラを \(K+1\) 回実行し、bitDP の遷移を行います。
- 時間計算量: \(O\big((K+1)\,(M \log N) + 2^K \cdot K^2\big)\)
- ダイクストラ部分: \((K+1)\) 回 × \(O(M \log N)\)
- bitDP 部分: \(O(2^K \cdot K^2)\)(\(2^{15} \cdot 15^2 \approx 7.4 \times 10^6\) で十分高速)
- 空間計算量: \(O\big((K+1)\,N + 2^K \cdot K\big)\)
- 各ダイクストラの距離配列と DP テーブル
実装のポイント
オーバーフローに注意: \(W_i\) は最大 \(10^9\)、経路は複数の辺を含むため、距離の合計は \(64\) ビット整数(
long long)で扱う必要があります。INFも加算でオーバーフローしないようLLONG_MAX/4程度にしておくと安全です。ダイクストラは隣接リスト + 優先度付きキューで実装します。
d > dist[u]のときは古い情報なのでスキップする、という枝刈りを忘れないようにしましょう。DP の参照は届け先のインデックス \(i\)(\(0\)〜\(K-1\))と地点番号 \(C_i\) を混同しないように注意します。
distC[i]は「\(i\) 番目の届け先からの距離配列」なので、別の届け先 \(C_j\) への距離はdistC[i][C[j]]のように地点番号で引きます。maskを昇順にループすれば、遷移元の状態が必ず先に確定するため、正しく DP が進みます。ソースコード
#include <bits/stdc++.h>
using namespace std;
int main(){
int N, M, K;
scanf("%d %d %d", &N, &M, &K);
vector<vector<pair<int,long long>>> g(N+1);
for(int i=0;i<M;i++){
int u,v; long long w;
scanf("%d %d %lld", &u, &v, &w);
g[u].push_back({v,w});
g[v].push_back({u,w});
}
vector<int> C(K);
for(int i=0;i<K;i++) scanf("%d", &C[i]);
const long long INF = LLONG_MAX/4;
auto dijkstra = [&](int s)->vector<long long>{
vector<long long> dist(N+1, INF);
priority_queue<pair<long long,int>, vector<pair<long long,int>>, greater<>> pq;
dist[s]=0;
pq.push({0,s});
while(!pq.empty()){
auto [d,u]=pq.top(); pq.pop();
if(d>dist[u]) continue;
for(auto [v,w]:g[u]){
if(dist[v]>d+w){
dist[v]=d+w;
pq.push({dist[v],v});
}
}
}
return dist;
};
// Dijkstra from node 1
vector<long long> distFrom1 = dijkstra(1);
// Dijkstra from each delivery point
vector<vector<long long>> distC(K);
for(int i=0;i<K;i++) distC[i] = dijkstra(C[i]);
// dist between deliveries
// dp
int full = (1<<K)-1;
vector<vector<long long>> dp(1<<K, vector<long long>(K, INF));
for(int i=0;i<K;i++){
dp[1<<i][i] = distFrom1[C[i]];
}
for(int mask=0; mask<=full; mask++){
for(int i=0;i<K;i++){
if(!(mask&(1<<i))) continue;
if(dp[mask][i]==INF) continue;
for(int j=0;j<K;j++){
if(mask&(1<<j)) continue;
long long nd = dp[mask][i] + distC[i][C[j]];
int nmask = mask|(1<<j);
if(dp[nmask][j] > nd) dp[nmask][j] = nd;
}
}
}
long long ans = INF;
for(int i=0;i<K;i++){
if(dp[full][i]==INF) continue;
ans = min(ans, dp[full][i] + distC[i][N]);
}
printf("%lld\n", ans);
return 0;
}
この解説は claude4.8opus-high によって生成されました。
posted:
last update: