Day 012-Q4 — 高度グラフ(Dominator Tree)

2026-04-25 赤色 / Phase 8 ★★★★★★★★ Lengauer-Tarjan

問題

有向グラフ G(頂点 0〜N-1、辺数 M)と根 r=0 が与えられる。各頂点 v の「即時支配者(immediate dominator)」を求めよ。u が v を支配するとは、r から v への全パスが u を通ること。idom[r] = -1。

制約

$2 \le N \le 2 \times 10^5$
$1 \le M \le 5 \times 10^5$
根 0 から全頂点に到達可能

入出力例

入力例 1

6 7
0 1
0 2
1 3
2 3
3 4
3 5
1 5

出力例 1

-1
0
0
0
3
3

ヒント (段階的開示)

ヒント1: 方向性
Lengauer-Tarjan アルゴリズムで O((N+M) α(N+M))。
ヒント2: アプローチ
1. DFS で pre-order 番号/2. semi-dominator を計算/3. immediate dominator を導出。
ヒント3: 誘導
Union-Find 風の path compression を用いた eval_link 関数で semi-dominator 計算を高速化。

模範解答 (Python)

import sys
from collections import defaultdict
input = sys.stdin.readline
sys.setrecursionlimit(300000)

def solve():
    N, M = map(int, input().split())
    adj = defaultdict(list)
    radj = defaultdict(list)
    for _ in range(M):
        u, v = map(int, input().split())
        adj[u].append(v)
        radj[v].append(u)

    root = 0
    order = []
    parent = [-1] * N
    semi = list(range(N))
    idom = [-1] * N
    vertex = [-1] * N
    label = list(range(N))
    ancestor = [-1] * N
    num = [-1] * N

    visited = [False] * N
    stack = [(root, -1, False)]
    while stack:
        v, p, back = stack.pop()
        if back:
            pass
        else:
            if visited[v]:
                continue
            visited[v] = True
            num[v] = len(order)
            vertex[len(order)] = v
            order.append(v)
            parent[v] = p
            for w in adj[v]:
                if not visited[w]:
                    stack.append((w, v, False))

    def compress(v):
        if ancestor[ancestor[v]] != -1:
            compress(ancestor[v])
            if num[semi[label[ancestor[v]]]] < num[semi[label[v]]]:
                label[v] = label[ancestor[v]]
            ancestor[v] = ancestor[ancestor[v]]

    def link(v, w):
        ancestor[w] = v

    def eval_(v):
        if ancestor[v] == -1:
            return v
        compress(v)
        return label[v]

    bucket = defaultdict(list)
    for i in range(len(order) - 1, 0, -1):
        w = vertex[i]
        for v in radj[w]:
            if num[v] == -1:
                continue
            u = eval_(v)
            if num[semi[u]] < num[semi[w]]:
                semi[w] = semi[u]
        bucket[vertex[num[semi[w]]]].append(w)
        link(parent[w], w)
        for v in bucket[parent[w]]:
            u = eval_(v)
            if num[semi[u]] < num[semi[v]]:
                idom[v] = u
            else:
                idom[v] = parent[w]
        bucket[parent[w]].clear()

    for i in range(1, len(order)):
        w = vertex[i]
        if idom[w] != vertex[num[semi[w]]]:
            idom[w] = idom[idom[w]]

    idom[root] = -1
    for v in range(N):
        print(idom[v])

solve()

Step-by-Step 解説

1DFS 木の構築
根から DFS で pre-order 番号を付ける。親 parent[v] も記録。
2Semi-dominator
逆順に処理。path compression で効率化。
3Bucket 処理
semi が同じ頂点をまとめて idom を推定。
4idom 修正
semi と idom が一致しなければ idom を辿って修正。

よくあるミス

ミス原因正しい書き方
到達不能頂点を含める未訪問の逆辺を処理if num[v] == -1: continue
再帰深度超過compress が深い反復版か setrecursionlimit
bucket clear 忘れ二重処理bucket[parent[w]].clear()

次のステップ

  • Dominator Tree を使った強連結成分検出
  • Virtual Tree(Auxiliary Tree)の構築

自己評価

自分の回答

気づき・メモ