您的位置:首页 > 其它

强连通分支——自制算法和经典算法的比较分析

2016-12-01 12:54 295 查看

简介

有向图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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: