問題
$N$ 頂点 $M$ 辺の無向重み付きグラフと $K$ 個のランドマーク頂点 $L_1,\dots,L_K$ が与えられる。頂点 $s$ から頂点 $t$ への最短距離を、ALT法(A* + ランドマーク + 三角不等式)で求めよ。
アルゴリズムの骨子
- 各ランドマーク $L_i$ から全頂点への最短距離 $d(L_i,\cdot)$ をダイクストラ法で前計算
- ヒューリスティック $h(v) = \max_i |d(L_i,t) - d(L_i,v)|$(三角不等式より $h(v)\le d(v,t)$、admissible)
- $f(v)=g(v)+h(v)$ の昇順に頂点を取り出すA*探索で最短距離を確定
入力形式
N M K
u_1 v_1 w_1
:
u_M v_M w_M
L_1 ... L_K
s t
制約
$2 \le N \le 2\times10^5$
$1 \le M \le 2\times10^5$
$1 \le K \le 10$
$1 \le w_i \le 10^9$
入出力例
入力例1
5 6 1
1 2 2
1 3 5
2 3 1
2 4 4
3 4 1
4 5 3
3
1 5出力例1
7最短経路 1→2→3→4→5(2+1+1+3=7)。ランドマーク3からの距離を使ったヒューリスティックで目的地方向を優先探索。
概念図:ランドマークによる下界
ヒント(段階的開示)
ヒント1(方向性)
通常のダイクストラ法は目的地を考慮せず一様に探索を広げる。目的地方向へのバイアスを、最短距離の正しさを崩さずに導入する方法を考えよ。
ヒント2(アプローチ)
A*探索は$f(v)=g(v)+h(v)$の順に処理するダイクストラ法の一般化であり、$h(v)$が真の残り距離を超えない下界(admissible)である限り正しさが保証される。ALT法はこの下界を事前計算したランドマーク距離+三角不等式で安く求める。複数ランドマークの最大値を取ればよりタイトな下界になる。
ヒント3(誘導)
landmark_dist = [dijkstra_from(N, adj, L) for L in landmarks]
def h(v, t):
best = 0
for ld in landmark_dist:
best = max(best, abs(ld[t] - ld[v]))
return best
g = [float('inf')] * (N + 1)
g[s] = 0
pq = [(h(s, t), s)]
# あとは f(v)=g(v)+h(v) をキーにした通常のダイクストラ法
模範解答 (Python)
import sys
import heapq
def dijkstra(N, adj, src):
dist = [float('inf')] * (N + 1)
dist[src] = 0
pq = [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist
def main():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
M = int(data[idx]); idx += 1
K = int(data[idx]); idx += 1
adj = [[] for _ in range(N + 1)]
for _ in range(M):
u = int(data[idx]); idx += 1
v = int(data[idx]); idx += 1
w = int(data[idx]); idx += 1
adj[u].append((v, w))
adj[v].append((u, w))
landmarks = [int(data[idx + i]) for i in range(K)]
idx += K
s = int(data[idx]); idx += 1
t = int(data[idx]); idx += 1
land_dist = [dijkstra(N, adj, L) for L in landmarks]
INF = float('inf')
def h(v):
best = 0
for ld in land_dist:
if ld[t] == INF or ld[v] == INF:
continue
best = max(best, abs(ld[t] - ld[v]))
return best
g = [INF] * (N + 1)
g[s] = 0
visited = [False] * (N + 1)
pq = [(h(s), s)]
while pq:
f, u = heapq.heappop(pq)
if visited[u]:
continue
visited[u] = True
if u == t:
break
for v, w in adj[u]:
ng = g[u] + w
if ng < g[v]:
g[v] = ng
heapq.heappush(pq, (ng + h(v), v))
print(g[t] if g[t] < INF else -1)
main()
Step-by-Step 解説
1許容的ヒューリスティックと正しさの保証
三角不等式$|d(L,t)-d(L,v)|\le d(v,t)$より、admissibleな下界が得られA*はダイクストラ法と同じ正しい距離を返す。
三角不等式$|d(L,t)-d(L,v)|\le d(v,t)$より、admissibleな下界が得られA*はダイクストラ法と同じ正しい距離を返す。
2複数ランドマークによる下界の強化
異なる位置の複数ランドマークからの下界の最大値を取ることで、どの方向のクエリでもタイトな下界を得る。
異なる位置の複数ランドマークからの下界の最大値を取ることで、どの方向のクエリでもタイトな下界を得る。
3A*探索の実装
優先度付きキューのキーをg(v)からf(v)=g(v)+h(v)に変えるだけで、あとは通常のダイクストラ法と同じロジック。
優先度付きキューのキーをg(v)からf(v)=g(v)+h(v)に変えるだけで、あとは通常のダイクストラ法と同じロジック。
4なぜ探索が減るのか
目的地から遠ざかる方向のノードはf(v)が大きくなり後回しにされる。
目的地から遠ざかる方向のノードはf(v)が大きくなり後回しにされる。
5計算量
前計算O(KM log N)、クエリ処理は通常のダイクストラ法と同じO(M log N)。
前計算O(KM log N)、クエリ処理は通常のダイクストラ法と同じO(M log N)。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| ヒューリスティックに負の値を許してしまう | $d(L,t)-d(L,v)$が負のままだと下界として不整合 | 必ず絶対値を取り複数ランドマークの最大値を使う |
| 訪問済み頂点をキューから取り出した際に再処理してしまう | キューに重複エントリが入る遅延削除方式の理解不足 | 取り出し時にvisitedを確認し確定済みならスキップする |
| 到達不能なランドマーク距離(inf)を混ぜて不正な値になる | 非連結グラフでinf同士の差分計算が発生 | infを含む差分計算はスキップし寄与しないランドマークとして無視する |
| h(v)を毎回全頂点分テーブル化しようとし計算量が爆発する | ALT法の利点(クエリに依存しない前計算)を勘違いする | h(v)はクエリ時にO(K)で都度計算し前計算はランドマーク距離配列のみに留める |
| 優先度付きキューのキーをg(v)のままにし見た目だけA*にしてしまう | ヒューリスティックの足し込みを実装し忘れる | heappushのキーに必ずh(v)を加算する |
次のステップ
- 発展: ランドマークの選び方(ランダム/最遠点貪欲/平面分割ベース)が探索効率にどう影響するか実験せよ。
- 発展2: 双方向A*(Q4の双方向ダイクストラ法と組み合わせる)にする場合、前向き・後ろ向きヒューリスティックの整合性(consistency)をどう保つか調べよ。