Tarjan のアルゴリズムの Python 実装

この記事では、Python コードを使用して、有向グラフの強連結成分を線形時間で解くための Tarjan のアルゴリズムを紹介します。


関連概念


有向グラフの例

強連結: 有向グラフでノードが互いに到達できる
強連結グラフ: 任意の 2 つのノードが強く連結されている有向グラフ 強連結
成分 (SCC): 有向グラフの非常に強く連結された部分グラフ

ローリンク値(LLV、中国語直訳:ローリンク値):深さ優先探索(DFS)プロセスにおいて、ノードが到達できる(自身を含む)最小ノード数

アルゴリズム処理


  • 深さ優先検索を開始します。未訪問のノードを訪問します。番号は自己増加し、LLV を番号として初期化し、ノードを訪問済みとしてマークし、スタックにプッシュします。
  • 深さ優先検索コールバック: 隣接ノード (前方) がスタックにある場合、現在のノードの LLV 値を更新します。
  • 隣接ノード アクセスの終了: 現在のノードが強連結成分 (SCC) の開始ノードである場合、現在のノードがポップされるまでスタック操作を実行します。

注:
すべての隣接ノードを訪れたノード (出次数) は、そのノードへのパス (入次数) を考慮しないため、一方向に接続されたノードが同じ強接続コンポーネントに含まれないようにします。

算例


計算例 1 は前の例と同じです。
算例 1

算例 2:
算例 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:
実行結果 1

算例 2:
走行結果2

おすすめ

転載: blog.csdn.net/Zhang_0702_China/article/details/130114031