简介
有向图G(V,E),圈是一个起始节点与终止节点相同的路径,即 a->….->a。找到所有圈,合并其节点,在图论里就是合并强连通分支。一开始没有找到有效算法,基于深度优先搜索,自制了一个算法。后来找到了一个开源代码【1】,代替了自制算法。本文比较了自制算法和经典算法的区别,总结了经验教训。
无向图的查圈算法
深度优先搜索算法是从已知节点出发,图的一种遍历算法。只要一个节点被同源两个路径访问,这两个路径则形成一个圈。因为每个节点只处理一次,所以时间与空间复杂度都是O(N)。其算法如下:
DFS(a) for undirected graph
stack.push(a)
while not stack.empty
i = stack.pop
mark i as accessed
{j} = adjacent(i)
if j is accessed
return stack[s,…,r] where stack[s]==stack[t]==j
stack.push({j})
对于无向图来说,每个 i的邻节点 j ,如果已经 accessed,那么形成一个圈。注意此时只找到了一个圈。简单地改造成多个圈的算法,在算法结束之后,合并节点,再启动。
有向图的查圈算法DGS
对于有向图来说,要记录从 a 到 i 的路径,才能判断 i的邻接点j 是否形成一个圈。算法如下:
DFS(a) for directed graph
stack.push(<{a}, 0>) //采用 stack 记录路径,集合下标从1开始
while not stack.empty
<set, k> = stack.top
mark set[k++] not in stack // in-stack为路径标记
if set[k] in stack
record, merge and pop stack[s,…,r] where stack[s]==stack[t]==set[k]
mark stack[…] not in stack
stack.push(<adjacent(stack[s-1].set[k]), 0>) // 合并节点后,重新搜索
continue
mark set[k] in stack // set[k]为当前路径上的节点
if k>|set|
stack.pop
else
stack.push(<adjacent(set[k]), 0>)
这个算法遍历了所有从 a 出发的路径,满足了查找圈的功能。然而要穷举所有路径,这个算法通常会很慢,需要采用一些加速策略。
查圈算法的加速策略
通常的策略是对图的简化,各种情况如下:
- 0入度、0出度的节点:直接消除。因为所有在圈上的节点的入度和出度数都大于0。这是一个迭代的过程,因为消除一个可能产生另外一个。
- 相邻的1入度节点和1出度节点:合并。因为通过其中一个节点必通过另外一个节点。
- 特殊情况,2度节点,即1入1出的节点。可以形成一个长链,合并成一个节点,加速搜索;也可能与另外节点形成一个圈,直接记录下来,合并。
- 无圈子图合并:可以减少搜索长度,代价是要引入额外的数据结构,而且对于成圈的子图集,还要重新验证其中节点的成圈性。
另一个策略是引入无向图的 accessed标记,每个节点只 access一次,但是会漏掉一些圈。之后再使用完全版本有向图算法。
还有一个策略是把节点分组,每组节点的生成子图内先行查找,然后在完整的图中查找。
Tarjan算法
Tarjan算法是三个经典算法之一【2,3】,基于数组的代码见【1】。
function strongconnect(v)
// Set the depth index for v to the smallest unused index
v.index := index
v.lowlink := index
index := index + 1
S.push(v)
v.onStack := true
// Consider successors of v
for each (v, w) in E do
if (w.index is undefined) then
// Successor w has not yet been visited; recurse on it
strongconnect(w)
v.lowlink := min(v.lowlink, w.lowlink)
else if (w.onStack) then
// Successor w is in stack S and hence in the current SCC
v.lowlink := min(v.lowlink, w.lowlink)
end if
end for
// If v is a root node, pop the stack and generate an SCC
if (v.lowlink = v.index) then
start a new strongly connected component
repeat
w := S.pop()
w.onStack := false
add w to current strongly connected component
while (w != v)
output the current strongly connected component
end if
end function
算法比较
- 算法框架:都是 DFS
- 堆栈
- DGS是 DFS 的栈,一个圈放在栈顶
- Tarjan 是一个特殊的栈,一个连通分支放在栈顶。
- 分解策略
- DGS 没有分解,只是基于节点
- Tarjan其实基于 DFS 的生成子图,无圈子图的每个节点都有(v.lowlink = v.index),直接就出栈了;有圈子图需要回溯到v.index最小的节点才有(v.lowlink = v.index)。
总结
开发大型图算法的注意事项:
- 关注经典算法,不要重复发明轮子。掌握利用生成子图的分解技巧。
- 关注有向图的统计信息,采取有针对性的加速策略。图的简化可能不是决定性的,但还是有一定效果的。
- 对于大型图,很难单步调试。需要自动检测一些约束条件来保证操作的正确性。
- 对记录下的圈节点,再次验证成圈条件
- 对节点合并操作
- 验证单个节点的前后一致性
- 整个图的一致性,例如边的连接关系
- 合并操作个数与合并集合基数的对应关系
- 这方面可参照“最弱前置条件”。
参考文献
[1] https://github.com/PetterS/SuiteSparse/blob/master/BTF/Source/btf_strongcomp.c
[2] https://en.wikipedia.org/wiki/Strongly_connected_component
[3] https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm