Day 020-Q3 — K番目最短路(Yen's Algorithm)

2026-05-03 赤色 Master / Phase 8+ ★★★★★★★★★ K-Shortest Path

問題

$N$ 頂点 $M$ 辺の有向重み付きグラフが与えられる。頂点 $1$ から頂点 $N$ への K番目に短い単純パス の長さを求めよ。

制約

$2 \le N \le 400$
$1 \le M \le N(N-1)$
$1 \le K \le 20$
$1 \le w_i \le 10^6$

入出力例

入力例 1

5 8 3
1 2 2
1 3 4
2 3 1
2 4 5
3 4 2
3 5 6
4 5 3
1 5 15

出力例 1

9

ヒント (段階的開示)

ヒント1: 方向性
Yen's Algorithm: 前回の最短路に deviation を加えて探索。$O(KN(M + N\log N))$。
ヒント2: アプローチ
$i$ 番目のパスから各頂点 $v$ を spur node として:1. $v$ より前は root path で固定。2. 以前と同じ辺を禁止。3. $v$ から N へダイクストラ。4. root+spur を候補として PQ に。
ヒント3: 誘導
def dijkstra(src, banned_nodes, banned_edges):
    # 通常のダイクストラに禁止条件を追加

模範解答 (Python)

import heapq
import sys

def solve():
    input_data = sys.stdin.read().split()
    idx = 0
    N, M, K = int(input_data[idx]), int(input_data[idx+1]), int(input_data[idx+2])
    idx += 3
    adj = [[] for _ in range(N + 1)]
    for _ in range(M):
        u, v, w = int(input_data[idx]), int(input_data[idx+1]), int(input_data[idx+2])
        idx += 3
        adj[u].append((v, w))

    def dijkstra(src, banned_nodes, banned_edges):
        dist = [float('inf')] * (N + 1)
        dist[src] = 0
        pq = [(0, src, [src])]
        while pq:
            d, v, path = heapq.heappop(pq)
            if d > dist[v]: continue
            if v == N: return d, path
            for u, w in adj[v]:
                if u in banned_nodes or (v, u) in banned_edges: continue
                nd = d + w
                if nd < dist[u]:
                    dist[u] = nd
                    heapq.heappush(pq, (nd, u, path + [u]))
        return float('inf'), []

    d, path = dijkstra(1, set(), set())
    if d == float('inf'):
        print(-1); return
    A = [(d, path)]
    B = []
    for i in range(K - 1):
        last_path = A[-1][1]
        for j in range(len(last_path) - 1):
            spur_node = last_path[j]
            root_path = last_path[:j+1]
            root_cost = sum(
                next(w for u2, w in adj[last_path[x]] if u2 == last_path[x+1])
                for x in range(j)
            )
            banned_edges = set()
            banned_nodes = set(root_path[:-1])
            for prev_dist, prev_path in A:
                if prev_path[:j+1] == root_path:
                    banned_edges.add((prev_path[j], prev_path[j+1]))
            for cand_dist, cand_path in B:
                if cand_path[:j+1] == root_path:
                    if j+1 < len(cand_path):
                        banned_edges.add((cand_path[j], cand_path[j+1]))
            spur_dist, spur_path = dijkstra(spur_node, banned_nodes, banned_edges)
            if spur_dist < float('inf'):
                total_cost = root_cost + spur_dist
                full_path = root_path[:-1] + spur_path
                if (total_cost, full_path) not in B:
                    heapq.heappush(B, (total_cost, full_path))
        if not B:
            print(-1); return
        best = heapq.heappop(B)
        A.append(best)
    print(A[K-1][0])

solve()

Step-by-Step 解説

1Yen's Algorithm の直感
K番目最短路は「前の最短路に少し変更を加えた」パス。各頂点を spur node として別経路を探す。
2Root path と Spur path
Root path: spur node より前(固定)。Spur path: spur node から目的地まで新経路。
3禁止条件
同じ root path で以前使ったパスと同じ「次の辺」を禁止 → 重複パス防止。
4計算量
$O(KN(M + N \log N))$。

よくあるミス

ミス原因正しい書き方
禁止条件で root path の比較が不正確list の比較コストtuple化してハッシュ比較
同じパスをキューに複数回追加重複チェック漏れ追加前に if not in B を確認
root_cost のインデックスずれj-1 と j の混同range(j) で丁寧に確認

次のステップ

  • 発展: 制約なし K番目最短路(拡張ダイクストラ)
  • K番目最短閉路

自己評価

自分の回答

気づき・メモ