E - 洞窟探検と地下水路 / Cave Exploration and Underground Waterways 解説 by admin
gpt-5.3-codex概要
V マスへの通常移動だけコストが重く(2分)、さらに同じ地下水路内なら 0 分移動できる、という「重み付き最短路」問題です。
V の連結成分を1つの「中継ノード」にまとめたグラフを作り、ダイクストラ法で最短時間を求めます。
考察
この問題の難しさは、V に関する2種類の移動が混在している点です。
- 通常移動で
Vに入る:\(1 + 1 = 2\) 分 V上にいるとき、同じ地下水路内の任意Vへ:\(0\) 分
重要な気づき1:地下水路内の 0 分移動は「連結成分単位」で扱える
同じ地下水路(V の連結成分)内なら、どの V 同士も実質 0 分で行き来できます。
そこで、各連結成分に対して スーパー頂点を1つ作り、
Vマス → 成分頂点(0)- 成分頂点 →
Vマス(0)
という辺を張ると、成分内ワープを自然に表現できます。
これにより、「同じ成分内の全 V 同士に 0 コスト辺を張る」必要がなくなり、辺数爆発を防げます。
重要な気づき2:通常移動は「移動先の種類」でコストが決まる
問題文より、上下左右の通常移動は基本1分で、移動先が V なら追加1分です。
よって通常移動の重みは
- 移動先が
V:2 - それ以外(
S,G,O):1
となります(X はそもそも通れない)。
素朴解法が厳しい理由
- 各
Vから同成分内すべてへ 0 コスト辺を張ると、最悪で二乗オーダーの辺数になりTLE/MLEの危険。 - BFSでは重み 0/1/2 が混在するためそのまま使えません(0-1 BFSも2があるので不可)。
したがって、
1. V 連結成分を作る
2. スーパー頂点化してグラフを疎に保つ
3. ダイクストラ法
が自然な方針です。
アルゴリズム
前処理:
Vの連結成分IDを振る- グリッド全体を見て、未訪問の
Vから BFS(またはDFS)で同成分を塗る。 comp_id[cell]に成分番号を記録。
- グリッド全体を見て、未訪問の
拡張グラフを構築
- 通常マス頂点:\(0 \ldots N-1\)(\(N=H \times W\))
- 成分スーパー頂点:\(N \ldots N+C-1\)(\(C\) は
V成分数) - 通行可能マス
uから隣接通行可能マスvへ通常移動辺を追加:- 重み \(2\)(
vがV) - 重み \(1\)(それ以外)
- 重み \(2\)(
uがVならu -> super(comp_id[u])に重み0super(comp_id[u]) -> uに重み0
ダイクストラ法
- 始点
Sから最短距離を計算。 Gの距離が更新されなければ到達不可でNO、そうでなければその値を出力。
- 始点
計算量
- 時間計算量: \(O((N + E)\log(N+C))\)
ここで \(N=H\times W\)。
隣接辺は各マス高々4本なので \(E=O(N)\)、またVと成分頂点の0辺も合計 \(O(N)\)。
よって全体として \(O(N\log N)\) 程度。 - 空間計算量: \(O(N + E)=O(N)\)
実装のポイント
マス \((r,c)\) を1次元ID
r*W+cに変換すると実装しやすいです。passable(ch) = (ch != 'X')で通行判定を統一するとバグを減らせます。通常移動コストは「現在地」ではなく移動先で決まる点に注意。
ダイクストラでは
if d != dist[u]: continueを入れて、古いヒープ要素を無視します。ソースコード
import sys
from collections import deque
def main():
input = sys.stdin.readline
H, W = map(int, input().split())
grid = [input().strip() for _ in range(H)]
N = H * W
def idx(r, c):
return r * W + c
start = goal = -1
for r in range(H):
row = grid[r]
for c, ch in enumerate(row):
if ch == 'S':
start = idx(r, c)
elif ch == 'G':
goal = idx(r, c)
# 1) Label connected components of V-cells
comp_id = [-1] * N
comp_count = 0
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
for r in range(H):
for c in range(W):
if grid[r][c] != 'V':
continue
i0 = idx(r, c)
if comp_id[i0] != -1:
continue
cid = comp_count
comp_count += 1
q = deque([i0])
comp_id[i0] = cid
while q:
v = q.popleft()
vr, vc = divmod(v, W)
for dr, dc in dirs:
nr, nc = vr + dr, vc + dc
if 0 <= nr < H and 0 <= nc < W and grid[nr][nc] == 'V':
ni = idx(nr, nc)
if comp_id[ni] == -1:
comp_id[ni] = cid
q.append(ni)
# 2) Build super graph:
# node 0..N-1 : cells
# node N..N+comp_count-1 : component supernodes
total_nodes = N + comp_count
adj = [[] for _ in range(total_nodes)]
def passable(ch):
return ch != 'X'
for r in range(H):
for c in range(W):
ch = grid[r][c]
if not passable(ch):
continue
u = idx(r, c)
# normal moves to neighbors
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < H and 0 <= nc < W:
nch = grid[nr][nc]
if passable(nch):
v = idx(nr, nc)
w = 2 if nch == 'V' else 1
adj[u].append((v, w))
# zero-cost teleport via V component
if ch == 'V':
cid = comp_id[u]
snode = N + cid
adj[u].append((snode, 0))
adj[snode].append((u, 0))
# 3) Dijkstra
INF = 10**30
dist = [INF] * total_nodes
dist[start] = 0
import heapq
hq = [(0, start)]
while hq:
d, u = heapq.heappop(hq)
if d != dist[u]:
continue
if u == goal:
break
for v, w in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(hq, (nd, v))
ans = dist[goal]
if ans >= INF:
print("NO")
else:
print(ans)
if __name__ == "__main__":
main()
この解説は gpt-5.3-codex によって生成されました。
投稿日時:
最終更新: