Day 080-Q5 — DAG 最長パス + 辞書順最小経路復元(トポロジカルDP)

2026-07-03 赤色 Master / Phase 8+ ★★★★★★★★★ DAG・DP・トポロジカルソート・辞書順最小・経路復元

問題

$N$ 頂点 $M$ 辺の DAG が与えられる。各辺には正の重みがある。

頂点 $s$ から頂点 $t$ への 最長重み付きパス の長さと、そのパスが複数ある場合は 辞書順最小の経路 を出力せよ。

$s$ から $t$ への有向パスが存在しない場合は -1 を出力。

制約

パラメータ範囲備考
$N$$2 \le N \le 2 \times 10^5$頂点数
$M$$1 \le M \le 5 \times 10^5$辺数
$w_i$$0 \le w_i \le 10^9$辺重み
グラフDAG が保証される

入出力例

入力例1

5 6 1 5
1 2 3
1 3 1
2 4 2
3 4 5
4 5 1
2 5 4

出力例1

7
1 2 5

最長パス長7は「1→3→4→5 (1+5+1)」と「1→2→5 (3+4)」の2通り。辞書順で「1 2 5 < 1 3 4 5」なので 1 2 5

概念図: 順方向 DP と逆方向 DP で辞書順最小経路を特定

DAG 最長パス — 順方向 dist + 逆方向 rev_dist で経路を絞り込む s=1 dist=0 2 dist=3 3 dist=1 4 dist=6 t=5 dist=7 3 1 2 5 1 4 辞書順最小経路: 1 → 2 → 5(長さ7 = 3+4) 候補: 1→3→4→5 (1+5+1=7) と 1→2→5 (3+4=7) — どちらも最長。辞書順で「1 2 5」が勝つ rev_dist[1]=7 rev_dist[2]=4 rev_dist[3]=6 rev_dist[4]=1 rev_dist[5]=0

ヒント

ヒント1(方向性)

DAG 上の最長パスは トポロジカル順序 に従った DP で $O(N + M)$ で計算できる。辞書順最小は追加の工夫が必要。

ヒント2(アプローチ)
  1. トポロジカルソート(Kahn's Algorithm)
  2. 順方向 DP: dist[v] = $s$ から $v$ への最長距離
  3. 逆方向 DP: rev_dist[v] = $v$ から $t$ への最長距離
  4. 経路復元: dist[cur] + w + rev_dist[v] == dist[t] を満たす辺のうち $v$ 最小を貪欲選択
ヒント3(ほぼ答え)
# 経路復元: 辞書順最小
path = [s]
cur = s
while cur != t:
    nxt = -1
    for v, w in sorted(adj[cur], key=lambda x: x[0]):
        if dist[cur] + w + rev_dist[v] == dist[t]:
            nxt = v
            break
    path.append(nxt)
    cur = nxt

模範解答

import sys
from collections import deque
input = sys.stdin.readline

def solve():
    N, M, s, t = map(int, input().split())
    s -= 1; t -= 1
    adj = [[] for _ in range(N)]
    in_deg = [0] * N
    for _ in range(M):
        u, v, w = map(int, input().split())
        u -= 1; v -= 1
        adj[u].append((v, w))
        in_deg[v] += 1

    # Kahn's トポロジカルソート
    topo = []
    q = deque(i for i in range(N) if in_deg[i] == 0)
    while q:
        u = q.popleft()
        topo.append(u)
        for v, _ in adj[u]:
            in_deg[v] -= 1
            if in_deg[v] == 0:
                q.append(v)

    INF = float('-inf')

    # 順方向 DP
    dist = [INF] * N
    dist[s] = 0
    for u in topo:
        if dist[u] == INF: continue
        for v, w in adj[u]:
            if dist[u] + w > dist[v]:
                dist[v] = dist[u] + w

    if dist[t] == INF:
        print(-1)
        return

    # 逆方向 DP
    rev_dist = [INF] * N
    rev_dist[t] = 0
    for u in reversed(topo):
        for v, w in adj[u]:
            if rev_dist[v] != INF and rev_dist[v] + w > rev_dist[u]:
                rev_dist[u] = rev_dist[v] + w

    print(dist[t])

    # 辞書順最小経路復元
    path = [s]
    cur = s
    while cur != t:
        nxt = -1
        for v, w in sorted(adj[cur], key=lambda x: x[0]):
            if (dist[cur] + w <= dist[t] and
                rev_dist[v] != INF and
                dist[cur] + w + rev_dist[v] == dist[t]):
                nxt = v
                break
        if nxt == -1:
            break
        path.append(nxt)
        cur = nxt

    print(*[x+1 for x in path])

solve()

Step-by-Step 解説

Step 1: トポロジカルソート

Kahn's Algorithm(BFS ベース)で入次数ゼロの頂点から順に処理。$O(N + M)$。

Step 2: 最長パス DP(順方向)

dist[v] を $s$ から $v$ への最長距離として初期化($-\infty$)。トポロジカル順に更新する。

Step 3: 最長パス DP(逆方向)

rev_dist[v] を $v$ から $t$ への最長距離として計算。トポロジカル順の逆順で処理する。

Step 4: 辞書順最小経路の復元

$cur$ にいるとき、次の頂点 $v$ は:

  • dist[cur] + w + rev_dist[v] == dist[t](最長パスの一部)
  • 上記を満たす $v$ のうち最小のものを選ぶ(辞書順最小)

辺を $v$ の番号でソートして最初にマッチしたものを貪欲選択。

Step 5: 時間計算量

トポロジカルソート + 順逆 DP: $O(N + M)$。経路復元: $O(N \log M)$(各ステップで辺をソート)。全体 $O((N + M) \log M)$。

よくあるミス

ミス原因正しい書き方
逆方向 DP を忘れる辞書順最小は「全体コンテキスト」が必要rev_dist を計算してから経路復元
前頂点番号のみで辞書順判定経路全体を比較しないと誤り逆 DP + 貪欲選択で正確に処理
未到達頂点の初期値が 0距離0と未到達を区別できない-inf または None で初期化
トポロジカル順序が s 前に t を含む$s→t$ パスがない場合dist[v] == INF のとき更新スキップ

次のステップ

  • 発展問題: $K$ 番目最短パス列挙(Yen's Algorithm + 辞書順)
  • 関連: DAG の最長パス + セグメント木区間更新 $O(N \log N)$

自己評価

理解度: / /

自分の回答:

気づき・メモ: