资讯专栏INFORMATION COLUMN

算法(第4版) Chapter 4.2 强联通性 Tarjan算法补充

maybe_009 / 3287人阅读

摘要:在实际的测试中,算法的运行效率也比算法高左右。此外,该算法与求无向图的双连通分量割点桥的算法也有着很深的联系。学习该算法,也有助于深入理解求双连通分量的算法,两者可以类比组合理解。固属于同一连通分支。

参考资料
http://blog.csdn.net/acmmmm/a...
https://www.byvoid.com/blog/s...
http://blog.csdn.net/nothi/ar...

在教材中有向图的强连通只提及了一种,其实还有另外两个经典的算法,因此做一个补充。

Tarjan算法 思路提点

tarjan的过程就是dfs过程

对图dfs一下,遍历所有未遍历过的点 ,会得到一个有向树,显然有向树是没有环的。

(注意搜过的点不会再搜) 则能产生环的只有 指向已经遍历过的点 的边

只有红色与绿色边有可能产生环。
对于深搜过程,我们需要一个栈来保存当前所在路径上的所有点(栈中所有点一定是有父子关系的)
再仔细观察红边与绿边,首先得到结论:红边不产生环,绿边产生环

对于红边,连接的两个点3、7没有父子关系,这种边称为横叉边
横叉边一定不产生环

对于绿边,连接的两个点6、4是父子关系,这种边称为后向边
环一定由后向边产生

图中除了黑色的树枝边,一定只有横叉边和后向边(不存在其他种类的边)

则以下考虑对于这两种边的处理和判断:

Stack = {1,2,3}。3没有多余的其他边,因此3退栈,把3作为一个强连通分量

再次深搜:

此时栈 Stack = {1,2,7}
发现红边指向了已经遍历过的点3 => 是上述的2种边之一
而3不在栈中
=> 3点与7点无父子关系
=> 该边为横叉边
=> 采取无视法。

继而7点退栈 产生连通分量{7}
继而2点退栈 产生连通分量{2}

再次深搜:

此时 Stack = {1,4,5,6}
发现绿边指向了已经遍历过的点4 => 是上述的2种边之一
而4在栈中
=> 4点与6点是父子关系
=> 该边为后向边
=> 4->6的路径上的点都是环。

实际情况可能更复杂:

出现了大环套小环的情况,显然我们认为最大环是一个强连通分量(即:{4,5,6,8} )

因而我们需要强化一下dfs过程,增添几个变量来记录父节点和后向边的情况

定义:

int dfn[N], low[N];

dfn[i] 表示 遍历到 i 点时是第几次dfs (有时也叫时间戳)

low[u] 表示 u 的子树 能连接到 [栈中] 最上端的点 的dfn值(换句话说,也就是最小的dfn)

Stack stack 上述的栈
int BelongTo[N] 强连通分量的ID

通俗语言解读:

dfn[i] 即我就是我,是数字不一样的烟火。每个点的ID(不是强连通分量的ID,而是每个点自己的身份标识ID),按时间顺序赋值。比如第一个寻访的dfn的值为 1,第二个寻访的DFN的值为 2,以此类推。可以通过比较大小来判断是爸爸还是儿子。(是后向边还是横插边?)

low[u] 如果是后向边的话连到哪个爸爸?记录下来爸爸的ID。

Stack 怎么判断是不是后向边呢?—>看在不在栈内。

Tarjan算法和伪代码

Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。
搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

定义dfn(u)为节点u搜索的次序编号(时间戳);
Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。

由定义可以得出,

low(u)=min
{
    dfn(u),
    low(v),(u,v)为树枝边,u为v的父节点 //回溯时用
    dfn(v),(u,v)为指向栈中节点的后向边(非横叉边) // 已访问过时用。没有想明白为什么是dfn(v)而不是low(v)?????????
}

当dfn(u)==low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

原因

在算法开始的时候,我们把 i 圧入栈中。根据 low[i] 和 dfn[i] 的定义我们知道,

如果 low[i] < dfn[i],则以 i 为顶点的子树中,有指向祖先的后向边,则说明 i 和 i 的父亲为在同一连通分支,也就是说留在栈中的元素都是和父结点在同一连通分支的。

如果 low[i] == dfn[i],则 i 为顶点的子树中没有后向边,那么由于 留在栈中的元素都是和父结点在同一连通分支的,我们可以知道,从栈顶到元素 i 构成了一个连通分支。显然,low[i]不可能小于dfn[i]

算法伪代码如下

tarjan(u)
{
    dfn[u]=low[u]=++index                      // 为节点u设定次序编号和low初值
    stack.push(u)                              // 将节点u压入栈中
    for each (u, v) in E                       // 枚举每一条边
        if (v is not visted)               // 如果节点v未被访问过
            tarjan(v)                  // 继续向下找
            Low[u] = min(low[u], low[v])
        else if (v in stack)                   // 如果节点v还在栈内
            Low[u] = min(low[u], dfn[v])
    if (dfn[u] == low[u])                      // 如果节点u是强连通分量的根
        repeat
            v = stack.pop                  // 将v退栈,为该强连通分量中一个顶点
            print v
        until (u== v)
}
Tarjan JAVA代码

复杂度

时间:O(N+M)

与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

public class Tarjan {
    private int[] id; // 强连通ID
    private int index_id = 0; // 强连通ID计数器
    private int[] dfn, low; //时间戳计数器
    private int index_dfn = 1; //时间戳初始化时默认为0。因此从1开始赋值,以区别是否访问过。
    private Stack stack= new Stack();
    private boolean[] onStack;
    
    public Tarjan(Digraph G) {
        onStack = new boolean[G.V()];
        id = new int[G.V()];
        dfn = new int[G.V()];
        low = new int[G.V()];

        for (int s = 0; s < G.V(); s++)
            if (dfn[s]==0) {
                tarjan(G, s);
                index_id++;
            }
    }

    private void tarjan(Digraph G, int u) {
        stack.push(u); //压入栈
        onStack[u] = true; //方便之后判断
        dfn[u] = low[u] = ++index_dfn; //时间戳赋值,并表示访问过了
        for (int w : G.adj(u)) {
            if (dfn[w] == 0) { //若w未被访问过
                tarjan(G, w); //继续深搜
                low[u] = Math.min(low[w], low[u]); //回溯,当前点u 选取包括自己的子树中最小的low值
            } else if (onStack[w] && dfn[w]
简单证明

首先,这边再重复一下什么是后向边:就是在深度优先搜索中,子孙指向祖先的边。
在一棵深度优先搜索树中,对于结点v, 和其父亲结点u而言,u,v 属于同一个强连通分支的充分必要条件
以v为根的子树中,有一条后向边指向u或者u的祖先

1、必要性

如果 u, v 属于同一个强连通分支则必定存在一条 u -> v 的路径和一条 v -> u的路径。
合并两条则有 u->v->v1->v2->..vn->u, 若顶点v1到vn都是v 的子孙,则有 vn->u这样一条后向边。
如果v1到vn 不全是vn的子孙,则必定有一个是u的祖先,我们不妨设vi为u的祖先,则有一条后向边 V[i-1] ->v[i]。

2、充分性

我们设 u1->u2->u3..->un->u->v->v1->v2..->vn。
我们假设后向边vn指向ui则有这样一个环:u[i]->u[i+1]...->u->v->v1->v2..->v[n-1]->v[n]->u[i]。
易知,有一条u->v的路径,同时有v->u的路径。固u,v属于同一连通分支。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66492.html

相关文章

  • 算法4Chapter 4.2 有向图

    摘要:只好特地拎出来记录证明一下算法步骤第一步在逆图上运行,将顶点按照逆后序方式压入栈中显然,这个过程作用在有向无环图上得到的就是一个拓扑排序作用在非上得到的是一个伪拓扑排序第二步在原图上按第一步的编号顺序进行。等价于已知在逆图中存在有向路径。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslat...

    曹金海 评论0 收藏0
  • 算法4Chapter 1

    摘要:由的位置决定乘以几,依次为,,,,,。递归算法递归算法就是本次结果用另外的参数调用自己。然而这个函数方法本身并没有用,因为方法中若传递参数为基本型如,在方法中对其值的改变并不会在主函数中产生影响。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslated By 谢路云 笔记 二分查找 Bin...

    Jacendfeng 评论0 收藏0
  • 算法4Chapter 4.4 最短路径

    摘要:相关操作就是判断的不等号符号改反,初始值设为负无穷副本的最短路径即为原图的最长路径。方法是同上面一样构造图,同时会添加负权重边,再将所有边取反,然后求最短路径最短路径存在则可行没有负权重环就是可行的调度。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslated By 谢路云Chapter ...

    leap_frog 评论0 收藏0
  • 算法4Chapter 4.1 无向图

    摘要:边仅由两个顶点连接,并且没有方向的图称为无向图。用分隔符当前为空格,也可以是分号等分隔。深度优先算法最简搜索起点构造函数找到与起点连通的其他顶点。路径构造函数接收一个顶点,计算到与连通的每个顶点之间的路径。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslated By 谢路云Chapter...

    kamushin233 评论0 收藏0
  • 算法4Chapter 5.1 字符串排序

    摘要:区别把数字对应成字符。这个是字符串的第位。稍作修改可适应不等长的字符串。因此增加一个组别,记录字符为空的频次。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslated By 谢路云Chapter 5 Section 1 字符串排序 参考资料http://blog.csdn.net/gua...

    Amio 评论0 收藏0

发表评论

0条评论

maybe_009

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<