強連結成分を縮めてDAGにする

有向グラフで相互に到達できる頂点を1つの成分にまとめます。成分を縮約したグラフには有向閉路がありません。

言語:Python / 計算量:発展実装 O(V+E)、基準検証 O(V(V+E))

前提:DAG:依存関係の順に処理する

考え方

  1. 1回目のDFSで帰りがけ順を記録。
  2. すべての辺を逆にします。
  3. 帰りがけ順の逆から逆グラフを探索し、成分番号を付けます。

具体例

0→1、1→0、1→2 なら {0,1} と {2}。縮約後は成分A→成分BのDAGです。

実装

g=[[1],[0,2],[]]
# この小例を到達可能性で確かめる
def reach(s):
    seen={s};stack=[s]
    while stack:
        v=stack.pop()
        for u in g[v]:
            if u not in seen:seen.add(u);stack.append(u)
    return seen
r=[reach(v) for v in range(3)]
print(1 in r[0] and 0 in r[1])

注意する条件

一方向の到達では同じ成分になりません。短い確認例は全始点探索。線形時間の実装は講義後半に掲載。

確認問題

例の強連結成分の個数は?

解答と理由

2

0と1は相互到達でき、2からは戻れません。

実装課題

小さい有向グラフで相互到達する頂点の組 i<j の数を求める。N≤80、M≤1000。N M と有向辺(0始まり)。全始点探索でよい。

入力:
3 3
0 1
1 0
1 2
出力:
1
参考実装
n,m=map(int,input().split());g=[[] for _ in range(n)]
for _ in range(m):
    u,v=map(int,input().split());g[u].append(v)
r=[]
for s in range(n):
    seen={s};stack=[s]
    while stack:
        v=stack.pop()
        for u in g[v]:
            if u not in seen:seen.add(u);stack.append(u)
    r.append(seen)
print(sum(j in r[i] and i in r[j] for i in range(n) for j in range(i+1,n)))

線形時間のSCC実装へ進む

帰りがけ順とは、ある頂点から行ける未探索の枝をすべて探索し終えたときに、その頂点を記録する順序です。先にスタックから取り出す順とは異なります。下の実装では (頂点, 次に見る辺の番号) を保存し、再帰から戻る動きを再現します。

逆グラフで帰りがけ順の逆から探索すると、まだ分類していない頂点のうち、ひとつの強連結成分だけを取り出せます。成分を縮約したDAGの順序と、2回目の探索方向が対応するためです。

すべての頂点と辺を各パスで定数回処理するので O(V+E)。戻り値 comp[v] は頂点vの成分番号、countは成分数です。番号が同じかどうかで相互到達を判定できます。

def scc(g):
    n = len(g)
    rg = [[] for _ in range(n)]
    for v in range(n):
        for u in g[v]:
            rg[u].append(v)
    seen = [False] * n
    order = []
    for s in range(n):
        if seen[s]:
            continue
        seen[s] = True
        stack = [(s, 0)]
        while stack:
            v, i = stack[-1]
            if i == len(g[v]):
                order.append(v)
                stack.pop()
            else:
                stack[-1] = (v, i + 1)
                u = g[v][i]
                if not seen[u]:
                    seen[u] = True
                    stack.append((u, 0))
    comp = [-1] * n
    count = 0
    for s in reversed(order):
        if comp[s] != -1:
            continue
        comp[s] = count
        stack = [s]
        while stack:
            v = stack.pop()
            for u in rg[v]:
                if comp[u] == -1:
                    comp[u] = count
                    stack.append(u)
        count += 1
    return comp, count

print(scc([[1], [0, 2], []]))

読了の記録・下書き・メモへ

関連する公式資料・課題