E - 配送ルートの最適化 / Optimization of Delivery Routes 解説 by admin
Claude 4.6 Opus (Thinking)概要
配送センター(届け先 \(1\))を出発し、\(N\) 箇所すべての届け先をちょうど1回ずつ訪問して配送センターに戻る巡回路のうち、燃料コスト(ユークリッド距離の二乗の総和)が最小となるものを求める、いわゆる巡回セールスマン問題(TSP)です。
考察
問題の本質
この問題は、\(N\) 頂点の完全グラフ上で、頂点 \(1\) を始点・終点とするハミルトン閉路(すべての頂点をちょうど1回ずつ通る閉路)のうち、辺の重みの合計が最小のものを求める問題です。
素朴なアプローチとその限界
最も単純な方法は、\(N\) 個の届け先を訪問する全順列を列挙することです。しかし、順列の数は \((N-1)!\) 通りあり、\(N = 16\) のとき \((16-1)! = 15! \approx 1.3 \times 10^{12}\) となり、到底間に合いません。
どう解決するか
訪問順序の全列挙では状態が爆発しますが、「どの届け先を訪問済みか(集合)」と「最後にどの届け先にいるか」だけを記録すれば、途中の訪問順序の詳細は不要です。これがビットマスクDP(ビットDP)の発想です。
\(N \leq 16\) なので、訪問済み集合を \(N\) ビットの整数で表現でき、状態数は \(2^N \times N\) で高々 \(2^{16} \times 16 = 1{,}048{,}576\) と十分小さくなります。
アルゴリズム
1. コスト行列の前計算
すべての届け先ペア \((i, j)\) について、移動コスト \((X_i - X_j)^2 + (Y_i - Y_j)^2\) を事前に計算しておきます。
2. ビットマスクDPの定義
- 状態: \(dp[S][u]\) = 「訪問済みの届け先の集合が \(S\)(ビットマスク)で、最後に訪問した届け先が \(u\) であるときの、最小コスト」
- 初期状態: \(dp[\{0\}][0] = 0\)(届け先 \(1\)、つまり0-indexedで頂点 \(0\) にいて、コストは \(0\))
- 遷移: 集合 \(S\) にいて頂点 \(u\) にいる状態から、まだ訪問していない頂点 \(v\)(\(v \notin S\))へ移動する: $\(dp[S \cup \{v\}][v] = \min(dp[S \cup \{v\}][v],\; dp[S][u] + \text{dist}[u][v])\)$
3. 答えの計算
すべての頂点を訪問した状態(\(S = \{0, 1, \ldots, N-1\}\)、ビットマスクで \(2^N - 1\))から、配送センター(頂点 \(0\))に戻るコストを加えた最小値が答えです: $\(\text{ans} = \min_{u=0}^{N-1} \left( dp[2^N - 1][u] + \text{dist}[u][0] \right)\)$
具体例
\(N = 3\)、頂点 \(0, 1, 2\) がある場合: - \(dp[001][0] = 0\)(頂点0のみ訪問、頂点0にいる) - \(dp[011][1] = \text{dist}[0][1]\)(頂点0→1と移動) - \(dp[111][2] = \text{dist}[0][1] + \text{dist}[1][2]\)(頂点0→1→2と移動) - 最後に \(dp[111][2] + \text{dist}[2][0]\) で頂点0に戻る
計算量
- 時間計算量: \(O(2^N \times N^2)\)(各状態 \(2^N \times N\) に対して、遷移先 \(N\) 個を試す)
- \(N = 16\) のとき約 \(2^{16} \times 16^2 \approx 1.7 \times 10^7\) で十分高速
- 空間計算量: \(O(2^N \times N)\)(DPテーブルのサイズ)
実装のポイント
届け先は1-indexedで与えられますが、コード内では 0-indexed で扱い、配送センターを頂点 \(0\) としています。
ビットマスク \(S\) のうち、頂点 \(0\)(配送センター)を含まない状態はスキップすることで無駄な計算を省いています(
if not (S & 1): continue)。DPの初期値を
INF(無限大)に設定し、到達不可能な状態を自然に除外しています。コスト行列を事前計算しておくことで、DP中の距離計算を \(O(1)\) にしています。
ソースコード
import sys
def main():
input_data = sys.stdin.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
coords = []
for i in range(N):
x = int(input_data[idx]); idx += 1
y = int(input_data[idx]); idx += 1
coords.append((x, y))
# Precompute distance (cost) matrix
dist = [[0] * N for _ in range(N)]
for i in range(N):
for j in range(N):
dx = coords[i][0] - coords[j][0]
dy = coords[i][1] - coords[j][1]
dist[i][j] = dx * dx + dy * dy
# TSP with bitmask DP
# dp[S][i] = minimum cost to visit exactly the set S of nodes, ending at node i
# Start at node 0 (1-indexed node 1)
INF = float('inf')
full = 1 << N
dp = [[INF] * N for _ in range(full)]
dp[1][0] = 0 # Start at node 0, visited set = {0}
for S in range(1, full):
if not (S & 1):
continue # Must include node 0
for u in range(N):
if dp[S][u] == INF:
continue
if not (S >> u & 1):
continue
for v in range(N):
if S >> v & 1:
continue
nS = S | (1 << v)
cost = dp[S][u] + dist[u][v]
if cost < dp[nS][v]:
dp[nS][v] = cost
# Answer: visit all nodes and return to node 0
full_mask = full - 1
ans = INF
for u in range(N):
if dp[full_mask][u] + dist[u][0] < ans:
ans = dp[full_mask][u] + dist[u][0]
print(ans)
main()
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: