Day 073-Q2 — SCC + DAG 上の最長重み付きパス

2026-06-26 赤色 Master / Phase 8+ ★★★★★★★★★ Kosaraju・縮退グラフ・DAG DP

問題

$N$ 頂点 $M$ 辺の有向グラフ $G$ が与えられる。各頂点 $v$ には価値 $a_v$ が設定されている(負の値もあり得る)。

強連結成分(SCC)内の頂点価値の総和を SCC の価値とする。縮退グラフ上で、価値の和が最大になる経路を求めよ。

すなわち、頂点の部分列 $v_1 \to v_2 \to \cdots \to v_k$(元グラフの辺が存在)で $\sum_{i=1}^{k} a_{v_i}$ の最大値を答えよ。同一 SCC 内の頂点は SCC の価値としてまとめて計算する。

制約

パラメータ範囲備考
$N$$1 \le N \le 10^5$頂点数
$M$$1 \le M \le 3 \times 10^5$辺数
$a_v$$-10^9 \le a_v \le 10^9$頂点価値(負あり)
自己ループなし、多重辺なし

入出力例

入力例1

5 5
3 -1 4 -1 5
1 2
2 3
3 1
1 4
4 5

出力例1

10

SCC{1,2,3}=6, SCC{4}=-1, SCC{5}=5。経路: C1→C4→C5 = 6-1+5=10

概念図: 縮退グラフ(Condensation Graph)

元グラフ SCC{1,2,3} val=6 1 a=3 2 a=-1 3 a=4 4 a=-1 5 a=5 縮退グラフ(DAG) C1 val = 6 C4 val = -1 C5 val = 5 dp=6 dp=5 dp=10 ★

ヒント

ヒント1(方向性)

Kosaraju または Tarjan の SCC アルゴリズムで強連結成分を求め、縮退 DAG を作る。その後 DAG 上でトポロジカルソート順に DP する。

ヒント2(アプローチ)
  1. SCC 分解: Kosaraju $O(N+M)$
  2. 各 SCC の価値 = 含まれる頂点の $a_v$ の総和
  3. 縮退 DAG 上でトポロジカルソート(BFS・Kahn法)
  4. $dp[c] = \text{val}[c] + \max(0, \max_{c' \to c} dp[c'])$
ヒント3(ほぼ答え)

Kosaraju 法のポイント: 1回目の DFS で元グラフの完了順を記録し、2回目は逆グラフ上で完了順の逆から DFS する。同一ツリーに入った頂点が同一 SCC。

dp = [scc_val[i] for i in range(c)]
for cu in topo:
    for cv in dag[cu]:
        dp[cv] = max(dp[cv], dp[cu] + scc_val[cv])
print(max(dp))

模範解答

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

def solve():
    N, M = map(int, input().split())
    a = list(map(int, input().split()))

    graph  = defaultdict(list)
    rgraph = defaultdict(list)
    for _ in range(M):
        u, v = map(int, input().split())
        u -= 1; v -= 1
        graph[u].append(v)
        rgraph[v].append(u)

    # Kosaraju SCC (反復 DFS)
    visited = [False] * N
    order = []
    for start in range(N):
        if not visited[start]:
            stack = [(start, iter(graph[start]))]
            visited[start] = True
            while stack:
                node, it = stack[-1]
                try:
                    nxt = next(it)
                    if not visited[nxt]:
                        visited[nxt] = True
                        stack.append((nxt, iter(graph[nxt])))
                except StopIteration:
                    order.append(node)
                    stack.pop()

    comp = [-1] * N
    c = 0
    for v in reversed(order):
        if comp[v] != -1:
            continue
        stack = [v]
        comp[v] = c
        while stack:
            node = stack.pop()
            for nxt in rgraph[node]:
                if comp[nxt] == -1:
                    comp[nxt] = c
                    stack.append(nxt)
        c += 1

    # SCC 価値計算
    scc_val = [0] * c
    for i in range(N):
        scc_val[comp[i]] += a[i]

    # 縮退 DAG 構築
    dag = defaultdict(set)
    for u in range(N):
        for v in graph[u]:
            cu, cv = comp[u], comp[v]
            if cu != cv:
                dag[cu].add(cv)

    # トポロジカルソート (Kahn 法)
    in_deg = [0] * c
    for cu in dag:
        for cv in dag[cu]:
            in_deg[cv] += 1

    queue = deque(i for i in range(c) if in_deg[i] == 0)
    topo = []
    while queue:
        node = queue.popleft()
        topo.append(node)
        for nxt in dag[node]:
            in_deg[nxt] -= 1
            if in_deg[nxt] == 0:
                queue.append(nxt)

    # DAG 上 DP
    dp = [scc_val[i] for i in range(c)]
    for cu in topo:
        for cv in dag[cu]:
            dp[cv] = max(dp[cv], dp[cu] + scc_val[cv])

    print(max(dp))

solve()

Step-by-Step 解説

Step 1: Kosaraju 法で SCC 分解

1回目の DFS(元グラフ): 全頂点の DFS 完了順を記録。2回目の DFS(逆グラフ): 完了順の逆から未訪問頂点に DFS → 同一ツリーが同一 SCC。計算量 $O(N+M)$。

Step 2: SCC の価値計算

各頂点 $v$ が属する SCC $c$ の価値に $a_v$ を加算。SCC 内の頂点は循環可能なので全頂点を経由できる。

Step 3: 縮退 DAG の構築

元グラフの辺 $(u, v)$ で $\text{comp}[u] \ne \text{comp}[v]$ なものを DAG の辺として追加。セットで重複除去。

Step 4: DAG 上の最長パス DP

初期値: $dp[c] = \text{val}[c]$。遷移: トポソート順に $dp[cv] = \max(dp[cv], dp[cu] + \text{val}[cv])$。答え: $\max_c dp[c]$。

よくあるミス

ミス原因正しい書き方
逆グラフを忘れる2回目 DFS は逆グラフ上rgraph[v].append(u)
同一 SCC 内の辺を DAG に含める自己ループになるif cu != cv で分岐
DP の初期値を 0 にする単頂点パスを見落とすdp[c] = scc_val[c] で初期化
答えを最後の SCC だけで取る終端は任意max(dp) を使う

次のステップ

  • 発展: 縮退 DAG 上でパスの数え上げ(トポソート + DP)
  • 発展: SCC 内に辺コストがある場合の最長パス
  • 発展: DAG 上の最短パス(負コスト許容)

自己評価

理解度:

自分の回答:

気づき・メモ: