E - 宅配ドライバーの巡回 / Delivery Driver's Route 解説 by admin
Gemini 3.0 Flash (Thinking)概要
この問題は、グラフ上の特定の地点(営業所 \(S\) および \(K\) 個の配達先 \(D_j\))をすべて巡回して元の場所に戻る最短経路を求める問題です。いわゆる「巡回セールスマン問題 (TSP)」のバリエーションですが、地点間の移動が最短経路を通る必要があるため、事前の最短距離計算と動的計画法(Bit DP)を組み合わせて解きます。
考察
1. 巡回セールスマン問題への帰着
この問題の難しい点は、「どの順番で配達先を回るのが最適か」という点です。配達先の数 \(K\) は最大で 15 と非常に小さいことに注目しましょう。もし、営業所と各配達先の間の「最短移動時間」がすべて分かっていれば、この問題は典型的な 巡回セールスマン問題 として解くことができます。
2. 地点間の最短距離を求める
まず、以下の地点を「重要な地点」としてまとめます。 - 営業所 \(S\) - 配達先 \(D_1, D_2, \dots, D_K\)
これらの地点は合計で \(K+1\) 個(最大 16 個)あります。グラフ上の任意の 2 地点間の最短距離を求める必要がありますが、制約 \(N=10,000, M=20,000\) では、全地点間の最短距離を求めるワーシャル・フロイド法 (\(O(N^3)\)) は使えません。 代わりに、各「重要な地点」を始点として ダイクストラ法 を合計 \(K+1\) 回実行することで、必要な地点間の距離を効率的に求めることができます。
3. 状態の管理(Bit DP)
\(K\) 個の配達先を訪れたかどうかを集合で管理します。 - \(S = \{ \text{訪れた配達先の集合} \}\) - \(v = \text{現在いる配達先}\) この 2 つを状態として持つ動的計画法(Bit DP)を利用します。
アルゴリズム
- グラフの構築: 与えられた道路情報を隣接リスト形式で保持します。
- 最短距離行列の作成:
営業所 \(S\) および各配達先 \(D_j\) の計 \(K+1\) 箇所からダイクストラ法を実行し、これら相互の最短距離を格納した \((K+1) \times (K+1)\) の行列
dist_matrixを作成します。 - Bit DP による最適化:
以下の DP テーブルを定義します。
dp[mask][i]:= 配達先の訪問状態がmask(ビット集合)であり、最後に配達先 \(D_i\) に到達したときの最小移動時間。- 初期化: 営業所 \(S\) から各配達先 \(D_i\) への移動時間を
dp[1 << i][i]にセットします。 - 遷移: 現在の状態
(mask, i)から、まだ訪れていない配達先 \(D_j\) へ移動する場合、dp[mask | (1 << j)][j] = min(dp[mask | (1 << j)][j], dp[mask][i] + dist_matrix[i+1][j+1])と更新します。
- 初期化: 営業所 \(S\) から各配達先 \(D_i\) への移動時間を
- 最終回答の計算:
すべての配達先を訪れた状態
dp[(1 << K) - 1][i]に、最後に配達先 \(D_i\) から営業所 \(S\) へ戻る時間を加算し、その最小値を出力します。
計算量
- ダイクストラ法: \(O(K \cdot M \log N)\)
- 1 回あたり \(O(M \log N)\) のダイクストラ法を \(K+1\) 回行います。
- Bit DP: \(O(K^2 \cdot 2^K)\)
- 状態数が \(K \cdot 2^K\) で、各状態からの遷移が \(K\) 通りです。
\(K=15\) のとき、\(K^2 \cdot 2^K \approx 225 \times 32,768 \approx 7.3 \times 10^6\) となり、制限時間内に十分収まります。
- 合計時間計算量: \(O(K \cdot M \log N + K^2 \cdot 2^K)\)
- 空間計算量: \(O(N + M + K \cdot 2^K)\)
実装のポイント
ダイクストラ法の高速化: 各ダイクストラ法の実行において、すべての「重要な地点」の最短距離が確定した時点で探索を打ち切る(早期終了)ことで、実行時間を短縮できます。
Bit DP のインデックス:
dist_matrixの 0 番目を営業所 \(S\)、1 番目以降を配達先 \(D_1 \dots D_K\) と対応させると、DP の遷移が書きやすくなります。Python での高速化:
sys.stdin.read().split()による一括入力や、heapqの直接参照などを利用すると、Python でも高速に動作します。ソースコード
import sys
import heapq
def solve():
# Read all input at once for faster processing
input_data = sys.stdin.read().split()
if not input_data:
return
ptr = 0
N = int(input_data[ptr])
ptr += 1
M = int(input_data[ptr])
ptr += 1
# Adjacency list representation of the graph
adj = [[] for _ in range(N + 1)]
for _ in range(M):
u = int(input_data[ptr])
ptr += 1
v = int(input_data[ptr])
ptr += 1
w = int(input_data[ptr])
ptr += 1
adj[u].append((v, w))
adj[v].append((u, w))
# Starting location and delivery locations
S = int(input_data[ptr])
ptr += 1
K = int(input_data[ptr])
ptr += 1
D = [int(input_data[ptr + i]) for i in range(K)]
# All points of interest (starting point + delivery points)
nodes = [S] + D
num_nodes = K + 1
# Shortest path distances between all pairs of nodes in 'nodes'
dist_matrix = [[0] * num_nodes for _ in range(num_nodes)]
# Map intersection ID to its index in the 'nodes' list for early exit in Dijkstra
is_node = [-1] * (N + 1)
for idx, node in enumerate(nodes):
is_node[node] = idx
heappop = heapq.heappop
heappush = heapq.heappush
INF = 10**18
# Run Dijkstra's algorithm from each point of interest
for i in range(num_nodes):
start_node = nodes[i]
distances = [INF] * (N + 1)
distances[start_node] = 0
pq = [(0, start_node)]
found_count = 0
while pq:
d, u = heappop(pq)
if d > distances[u]:
continue
# Count how many target points of interest we've found
if is_node[u] != -1:
found_count += 1
# If all targets are reached from this source, we can stop the search
if found_count == num_nodes:
break
for v, w in adj[u]:
new_dist = d + w
if distances[v] > new_dist:
distances[v] = new_dist
heappush(pq, (new_dist, v))
# Store distances to other points of interest in the matrix
for j in range(num_nodes):
dist_matrix[i][j] = distances[nodes[j]]
# Traveling Salesperson Problem (TSP) using Dynamic Programming
# dp[mask][i] is the minimum distance to visit delivery nodes in 'mask', ending at delivery node D[i]
dp = [[INF] * K for _ in range(1 << K)]
# Initial state: travel from starting point S to each delivery node
for i in range(K):
# dist_matrix[0][i+1] is distance from nodes[0] (S) to nodes[i+1] (D[i])
dp[1 << i][i] = dist_matrix[0][i+1]
# Transition: visit more delivery nodes
for mask in range(1, 1 << K):
dp_mask = dp[mask]
for i, d_i in enumerate(dp_mask):
if d_i >= INF:
continue
# Current distance to reach D[i] after visiting nodes in 'mask'
dist_matrix_i_plus_1 = dist_matrix[i+1]
for j in range(K):
# If delivery node D[j] has not been visited yet
if not (mask & (1 << j)):
next_mask = mask | (1 << j)
new_dist = d_i + dist_matrix_i_plus_1[j+1]
if dp[next_mask][j] > new_dist:
dp[next_mask][j] = new_dist
# Final step: return to starting point S after visiting all delivery nodes
ans = INF
full_mask = (1 << K) - 1
final_dp = dp[full_mask]
for i in range(K):
# dist_matrix[i+1][0] is the distance from D[i] to S
res = final_dp[i] + dist_matrix[i+1][0]
if res < ans:
ans = res
# Output the minimum total travel time
print(ans)
if __name__ == '__main__':
solve()
この解説は gemini-3-flash-thinking によって生成されました。
投稿日時:
最終更新: