この記事では、Python コードを使用して、有向グラフの強連結成分を線形時間で解くための Tarjan のアルゴリズムを紹介します。
関連概念
強連結: 有向グラフでノードが互いに到達できる
強連結グラフ: 任意の 2 つのノードが強く連結されている有向グラフ 強連結
成分 (SCC): 有向グラフの非常に強く連結された部分グラフ
ローリンク値(LLV、中国語直訳:ローリンク値):深さ優先探索(DFS)プロセスにおいて、ノードが到達できる(自身を含む)最小ノード数
アルゴリズム処理
- 深さ優先検索を開始します。未訪問のノードを訪問します。番号は自己増加し、LLV を番号として初期化し、ノードを訪問済みとしてマークし、スタックにプッシュします。
- 深さ優先検索コールバック: 隣接ノード (前方) がスタックにある場合、現在のノードの LLV 値を更新します。
- 隣接ノード アクセスの終了: 現在のノードが強連結成分 (SCC) の開始ノードである場合、現在のノードがポップされるまでスタック操作を実行します。
注:
すべての隣接ノードを訪れたノード (出次数) は、そのノードへのパス (入次数) を考慮しないため、一方向に接続されたノードが同じ強接続コンポーネントに含まれないようにします。
算例
計算例 1 は前の例と同じです。
算例 2:
コード
node.py
from typing import List
class Node(object):
def __init__(self, id: int, parents: List[int], descendants: List[int]) -> None:
"""
node initialise
:param id: node ID
:param parents: from which nodes can come to current node directly
:param descendants: from current node can go to which nodes directly
"""
self.id = id
self.parents = parents
self.descendants = descendants
アルゴリズム.py
from typing import Dict
from node import Node
class Tarjan(object):
"""
Tarjan's algorithm
"""
def __init__(self, nodes: Dict[int, Node]) -> None:
"""
data initialise
:param nodes: node dictionary
"""
self.nodes = nodes
# intermediate data
self.unvisited_flag = -1
self.serial = 0 # serial number of current node
self.num_scc = 0 # current SCC
self.serials = {
i: self.unvisited_flag for i in nodes.keys()} # each node's serial number
self.low = {
i: 0 for i in nodes.keys()} # each node's low-link value
self.stack = [] # node stack
self.on_stack = {
i: False for i in nodes.keys()} # if each node on stack
# run algorithm
self.list_scc = [] # final result
self._find_scc()
def _find_scc(self):
"""
algorithm main function
"""
for i in self.nodes.keys():
self.serials[i] = self.unvisited_flag
for i in self.nodes.keys():
if self.serials[i] == self.unvisited_flag:
self._dfs(node_id_at=i)
# result process
dict_scc = {
}
for i in self.low.keys():
if self.low[i] not in dict_scc.keys():
dict_scc[self.low[i]] = [i]
else:
dict_scc[self.low[i]].append(i)
self.list_scc = list(dict_scc.values())
def _dfs(self, node_id_at: int):
"""
algorithm recursion function
:param node_id_at: current node ID
"""
self.stack.append(node_id_at)
self.on_stack[node_id_at] = True
self.serials[node_id_at] = self.low[node_id_at] = self.serial
self.serial += 1
# visit all neighbours
for node_id_to in self.nodes[node_id_at].descendants:
if self.serials[node_id_to] == self.unvisited_flag:
self._dfs(node_id_at=node_id_to)
# minimise the low-link number
if self.on_stack[node_id_to]:
self.low[node_id_at] = min(self.low[node_id_at], self.low[node_id_to])
# After visited all neighbours, if reach start node of current SCC, empty stack until back to start node.
if self.serials[node_id_at] == self.low[node_id_at]:
node_id = self.stack.pop()
self.on_stack[node_id] = False
self.low[node_id] = self.serials[node_id_at]
while node_id != node_id_at:
node_id = self.stack.pop()
self.on_stack[node_id] = False
self.low[node_id] = self.serials[node_id_at]
self.num_scc += 1
main.py
from node import Node
from algorithm import Tarjan
# params
# case 1
num_node = 8
connections = [
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 1, 0, 0]
]
# # case 2
# num_node = 6
# connections = [
# [0, 1, 1, 0, 0, 0],
# [0, 0, 0, 1, 0, 0],
# [0, 0, 0, 1, 1, 0],
# [1, 0, 0, 0, 0, 1],
# [0, 0, 0, 0, 0, 1],
# [0, 0, 0, 0, 0, 0]
# ]
# nodes
nodes = {
i: Node(id=i, parents=[j for j in range(num_node) if connections[j][i]],
descendants=[j for j in range(num_node) if connections[i][j]]) for i in range(num_node)}
# algorithm
tarjan = Tarjan(nodes=nodes)
print()
print("strongly connected components:")
for scc in tarjan.list_scc:
print(scc)
print()
運用実績
算例 1:
算例 2: