公式
E - 宅配ドライバーの巡回 / Delivery Driver's Route 解説 by admin
GPT 5.2 High概要
営業所 \(S\) から出発して \(K(\le 15)\) 個の配達先をすべて訪問し、再び \(S\) に戻る最小移動時間を求める問題です。
「グラフ上の巡回(TSP)」を、配達先が少ないことを利用して解きます。
考察
重要な気づき
- 道路ネットワークは \(N\le 10^4, M\le 2\times 10^4\) と大きいですが、訪れるべき地点は \(S\) と配達先 \(K\) 個の合計 \(K+1\le 16\) 個しかないのがポイントです。
- 配達順は自由なので、結局やりたいことは
「\(S\) と配達先だけを頂点とする完全グラフ」を作り、その上で最短巡回を求める
ことに近いです。
素朴な方法がダメな理由
- 配達順を全列挙すると \(K!\) 通り。\(K=15\) なら \(15!\approx 1.3\times 10^{12}\) で不可能です(TLE)。
- また、順番を決めるたびに「次の目的地までの最短路」を元の大きいグラフで都度探索すると、さらに重くなります。
解決方針
- まず、必要な地点(\(S\) と配達先)同士の最短距離だけを集める
→ 各地点からダイクストラ法を実行して距離表を作る。 - その距離表を使って、配達先 \(K\) 個に対する巡回問題を
bitDP(巡回セールスマン問題の典型DP)で解く。
例:配達先が 3 件(A,B,C)なら、
「S→A→C→B→S」などの順序を直接全列挙せず、
「今どこまで訪問したか(集合)」と「最後にいる配達先」を状態にして最小値を更新します。
アルゴリズム
1. 距離表の作成(ダイクストラ)
- 対象地点
terminals = [S] + D(サイズ \(t=K+1\))を用意。 - 各
terminals[i]を始点にダイクストラ法を行い、元グラフ上の最短距離distを得る。 - 距離表
dist_mat[i][j] = dist[terminals[j]]を埋める。
これで「必要な地点間の移動コスト」だけが取り出せます。
2. bitDP で最短巡回(TSP)
- 配達先を \(1..K\) 番、営業所を \(0\) 番(
terminals[0]=S)とします。 - 状態:
mask:どの配達先を訪れたかを表すビット集合(サイズ \(2^K\))last:最後にいる配達先(\(1..K\))
- DP定義:
- \(dp[mask][last] =\)
「集合maskの配達先をすべて訪れ、最後がlastであるときの最小時間」
- \(dp[mask][last] =\)
- 初期化:
- 最初に 1 件だけ行く場合
\(dp[1<<(i-1)][i] = dist\_mat[0][i]\)
- 最初に 1 件だけ行く場合
- 遷移:
- 次に行く配達先
nxtを選ぶ
\(dp[mask\cup\{nxt\}][nxt] = \min(dp[mask\cup\{nxt\}][nxt],\ dp[mask][last] + dist\_mat[last][nxt])\)
- 次に行く配達先
- 答え:
- 全部訪問した
full = (1<<K)-1から営業所へ戻る
\(\min_{last} \left(dp[full][last] + dist\_mat[last][0]\right)\)
- 全部訪問した
計算量
- 時間計算量:
- ダイクストラを \(K+1\) 回:\(O((K+1)\, M \log N)\)
- bitDP:\(O(2^K \cdot K^2)\)
よって全体で \(O((K+1)\, M \log N + 2^K K^2)\)
- ダイクストラを \(K+1\) 回:\(O((K+1)\, M \log N)\)
- 空間計算量:
- グラフ:\(O(N+M)\)
- 距離配列(ダイクストラ用):\(O(N)\)(ただしコードでは各始点の dist を保持し、合計 \(O((K+1)N)\))
- DP:\(O(2^K \cdot K)\)
よって概ね \(O(N+M + (K+1)N + 2^K K)\)
- グラフ:\(O(N+M)\)
実装のポイント
頂点数が大きいので、最短距離は必ずダイクストラ法(優先度付きキュー)を使います(辺重みが正)。
bitDP の
maskは「配達先のみ」を管理し、営業所 \(S\) は含めない設計にすると実装が簡潔です。距離は最大で大きくなり得るため、
INF = 10**30のように十分大きい値を使います。入力が大きいので
sys.stdin.buffer.read()でまとめて読むと高速です。ソースコード
import sys
import heapq
def dijkstra(start, g, n):
INF = 10**30
dist = [INF] * (n + 1)
dist[start] = 0
hq = [(0, start)]
push = heapq.heappush
pop = heapq.heappop
while hq:
d, v = pop(hq)
if d != dist[v]:
continue
for to, w in g[v]:
nd = d + w
if nd < dist[to]:
dist[to] = nd
push(hq, (nd, to))
return dist
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
it = iter(data)
N = next(it)
M = next(it)
g = [[] for _ in range(N + 1)]
for _ in range(M):
u = next(it); v = next(it); w = next(it)
g[u].append((v, w))
g[v].append((u, w))
S = next(it)
K = next(it)
D = [next(it) for _ in range(K)]
terminals = [S] + D # index 0 is S, 1..K are deliveries
t = K + 1
dist_mat = [[0] * t for _ in range(t)]
all_dists = []
for s in terminals:
all_dists.append(dijkstra(s, g, N))
for i in range(t):
di = all_dists[i]
for j in range(t):
dist_mat[i][j] = di[terminals[j]]
INF = 10**30
size = 1 << K
dp = [[INF] * (K + 1) for _ in range(size)]
for i in range(1, K + 1):
dp[1 << (i - 1)][i] = dist_mat[0][i]
for mask in range(size):
row = dp[mask]
for last in range(1, K + 1):
cur = row[last]
if cur >= INF:
continue
for nxt in range(1, K + 1):
bit = 1 << (nxt - 1)
if mask & bit:
continue
nm = mask | bit
val = cur + dist_mat[last][nxt]
if val < dp[nm][nxt]:
dp[nm][nxt] = val
full = size - 1
ans = INF
for last in range(1, K + 1):
val = dp[full][last] + dist_mat[last][0]
if val < ans:
ans = val
print(ans)
if __name__ == "__main__":
main()
この解説は gpt-5.2-high によって生成されました。
投稿日時:
最終更新: