基本概念
要理解Tarjan算法,必须了解以下两个概念,发现时间戳discovery time与低连接值low-link value。
发现时间戳这个很容易理解,Kosajaru算法没有使用到发现时间戳,但是使用了结束时间戳。低连接值是一个强连通分量(以下简称SCC)里所有节点的最小发现时间戳。也就是说一个SCC内,所有的点的低连接值相同。
图拉真的算法其实很简单,在DFS过程中,如果没有环存在,那么低连接值是等于发现时间戳的。什么时候不会相等呢?产生回边的时候,如果有回边,那么就更新当前低连接值为邻居的低连接值,也就是父级节点的低连接值。在递归过程中,低连接值不断传递。如果没有遇到回边,那么节点就会被弹出,形成一个SCC。
实例
如以下图:
首先一直DFS下去,遇到一个回边:
将其改成1的low值,也就是改成回边的low值:
再是3:
再是7:
再是5,需要注意的是5不会更新1的low值,因为1已经被完全处理完了,已经弹出了栈:
最后是6:
Python实现
# _*_ coding:utf-8 _*_
class UnweightedGraph:
def __init__(self, vertices, edges):
self.__vertices = vertices
self.__edges = edges
self.time = 0
n = len(self.__vertices)
self.disc = [0] * n
self.low = [0] * n
self.__pos = None
# 四种颜色
UNVISITED = 0
VISITED = 1
FINISHED = 2
POPPED = 3
def trajan(self):
n = len(self.__vertices)
self.time = 0
colors = [UnweightedGraph.UNVISITED] * n
stack = []
scc_list = []
for root in range(n):
self.dfs(root, colors, stack, scc_list)
return scc_list
def dfs(self, root, colors, stack, scc_list):
if colors[root] != UnweightedGraph.UNVISITED:
return self.low[root]
colors[root] = UnweightedGraph.VISITED
self.time += 1
self.disc[root] = self.time
self.low[root] = self.disc[root]
stack.append(root)
for neighbour in self.__edges[root]:
if colors[neighbour] == UnweightedGraph.POPPED:
continue
low_num = self.dfs(neighbour, colors, stack, scc_list)
if low_num < self.low[root]:
self.low[root] = low_num
colors[root] = UnweightedGraph.FINISHED
if self.low[root] == self.disc[root]:
scc_list.append(UnweightedGraph.get_scc(colors, stack, root))
return self.low[root]
@staticmethod
def get_scc(colors, trajan_stack, v):
scc = []
while len(trajan_stack) > 0:
x = trajan_stack.pop()
scc.append(x)
colors[x] = UnweightedGraph.POPPED
if x == v:
break
return scc
def to_dot(self):
s = 'digraph s {\nlayout=fdp\nnode[shape=box]\n'
for v in self.__vertices:
s += f'"{self.__vertices[v]}"[label="{self.__vertices[v]}(d={self.disc[v]},l=[{self.low[v]}])";pos="{self.__pos[v]}"];\n'
for i, e in enumerate(self.__edges):
for t in e:
s += f'\"{self.__vertices[i]}\"->"{self.__vertices[t]}";\n'
s += '}\n'
return s
@property
def vertices(self):
return self.__vertices
@vertices.setter
def vertices(self, value):
self.__vertices = value
@property
def edges(self):
return self.__edges
@edges.setter
def edges(self, value):
self.__edges = value
@property
def pos(self):
return self.__pos
@pos.setter
def pos(self, value):
self.__pos = value
测试代码
import unittest
from com.youngthing.graph.trajan import UnweightedGraph
class MyTestCase(unittest.TestCase):
def test_kosajaru(self):
# 以这个图为例子:
vertex = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M']
edges = [
[5],
[0, 2, 6],
[3, 6],
[8],
[3, 9],
[1],
[7, 10, 11],
[3, 8, 11],
[2, 12],
[4, 8],
[11],
[12],
[11],
]
graph = UnweightedGraph(vertex, edges)
scc_list = graph.trajan()
for scc in scc_list:
print([graph.vertices[x] for x in scc])
print(graph.to_dot())
def test_nyu_data(self):
# 以这个图为例子:
vertex = [0, 1, 2, 3, 4, 5, 6, 7]
edges = [
[1, 2],
[3],
[5, 6],
[4], # 3
[1], # $
[1, 7], # 5
[5], # 6
[2], # 7
]
graph = UnweightedGraph(vertex, edges)
graph.pos = ["0,0!", "-2,-1!", "2,-1!", "-4,-2!", "-2,-2!", "0,-2!", "2,-2!", "4,-3!"]
scc_list = graph.trajan()
for scc in scc_list:
print([graph.vertices[x] for x in scc])
print(graph.to_dot())
if __name__ == '__main__':
unittest.main()
测试结果完全正确:
[4, 3, 1]
[6, 7, 5, 2]
[0]