zoukankan      html  css  js  c++  java
  • 强连通分量的三个求法

    这里主要谈及强连通分量(以下简称SCC,strongly connected component)三种常见的求法(以下涉及的图均为有向图),即Kosaraju、Tarjan和Gabow。三种算法背后的基础思想都是DFS,只是它们通过DFS获得了不同的信息。各位大哥大姐继续往下读之前,最好对DFS相关的概念和性质比较熟悉,例如,什么叫做<a title="DFS" href="http://en.wikipedia.org/wiki/Depth-firstsearch" target="blank">反向边、交叉边</a>。

    Kosaraju

    我就不八卦这大哥了。Kosaraju的方法也就是导论第二版中文22章中讲的方法, 即广为人知的两遍DFS。Kosaraju算法说白了就是先对原图来一遍DFS, 再把所有边的方向都倒过来,按照刚才DFS求出的结点完成时间的逆序,再来一遍DFS。

    那么为啥可以这么做呢?

    这就需要一个性质来帮忙,即当A、B为两个SCC,且存在有向边(a, b),其中a∈A,b∈B, 那么必然有:A的完成时间晚于B(一个SCC的完成时间表示该SCC中所有结点完成时间最晚的一个)。

    可以简单证明一下:

    如果我们在第一遍DFS的时候,A先于B被访问,且A中的第一个被访问到的结点是x, 那A和B中所有的结点显然都能在x之后被访问到。于是x的完成时间要晚于B中任何一个结点, 从而A的完成时间晚于B。

    如果B先于A被访问,由于A和B是不同的两个SCC,而且只有A到B的边,于是B就不能到达A。 那么当B中的结点被访问完之后,A中的点仍然处于为访问状态,自然A的完成时间也就晚于B了。

    所以在第二遍DFS时,第一次取到的那个完成时间最晚的结点u, 它所在的SCC在转置图中就不能有指向外的边。于是对转置图的第二遍DFS, 从u开始,便能轻易走遍所有处于同一SCC的结点。后续的遍历步骤也就类似了。

    时间复杂度自然是算在两次DFS头上,O(V+E)。

    Tarjan

    Tarjan貌似跟Hopcroft都是Cornell的大神。总的来说, Tarjan算法基于一个观察,即:同处于一个SCC中的结点必然构成DFS树的一棵子树。 我们要找SCC,就得找到它在DFS树上的根。

    那么怎么找呢?

    考虑一下,如果DFS访问到了某个结点u,又顺着u来到了结点v, 但从v发出了一条反向边,指向了u的前驱w,那根据DFS的性质, u->v->w->u构成了一个环。这一堆东西必然处于同一个SCC。 所以某个要找到SCC子树的根,就得找那个在DFS树中最早被发现的结点,且这个结点要与它的一堆后继结点形成环。

    这时候DFS的特性就派上用场了。最早发现的结点可以通过记录发现时间来实现,而反向边的判断可以通过结点颜色,即访问状态来实现。 定义一个结点的low值为:从该节点的子树结点可达的,尚未求出属于哪个SCC的结点的最早访问时间。 由于SCC构成子树,所以求没求出某个结点所在的SCC用栈来刻画就可以了: 每次访问到一个结点u,记录发现时间visit,并将它推到栈里去。 如果从u可达的结点v没访问过,那么访问v,用v的low值更新u; 否则,如果v已访问过,那就看看它在不在栈中。如果在,说明还没确定v到底属于哪个SCC, 这时(u, v)就是一条反向边了,根据v的visit值,更新u的low值即可。 最后回到u结点时,如果u的low值和visit相等了,显然u就是我们要找的根节点了。 从栈里把u和其上所有结点弹出来,这一堆东西就在一个SCC里了。上伪代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // n: number of nodes in the graph
    
    // visit[i]: discovery time of node i
    // low[i]: low-value of node i
    // time: the time stamp
    // S: the stack
    
    time <- 1
    
    FIND_SCC(G):
    for i<-1 to n:
        if visit[i] is not defined:
            TARJAN(i)
    
    TARJAN(u):
    visit[u] <- time
    low[u] <- time
    time <- time + 1
    push(S, u)
    
    for each edge (u, i) in the graph:
        if visit[i] is not defined:
            TARJAN(i)
            low[u] <- min{ low[u], low[i] }
        else if i is in S:
            low[u] <- min{ low[u], visit[i] }
    
    // things popped here are in the same SCC
    if visit[u] == low[u]:
        pop all node above u on stack including u
    

    每个结点入栈一次出栈一次,每条边访问一次,O(V+E)。

    Gabow

    Gabow与Tarjan的思路是一致的。但Gabow使用了另一个栈来找出SCC子树的根。 Gabow使用的栈S与Tarjan一样,保存尚未决定属于哪个SCC的结点; 栈P保持如下性质:栈顶结点始终具有最小的visit值, 即保持栈顶元素的visit值小于等于当前发现的反向边指向的祖先结点的visit值。

    栈S和P都随着DFS的进行增长。若当前正在访问结点u,从u可达点v, 先将u压入两个栈中。这一步骤相当于Tarjan中初始化一个结点的low值为当前visit值。 如果v没有访问过,则访问v;否则判断v是否在S栈中。 如果在,那么(u, v)为反向边,此时从P栈顶弹出那些晚于v被发现的结点。为啥? 因为此时v是u的后继结点,我们得找出以u为根的子树结点能到达的最早访问的结点, 类似于Tarjan算法中对low值的更新。

    再一次回到u时,若P栈栈顶的元素就是u,表明u就是SCC子树的根。 与Tarjan类似,从S栈中弹出元素即找到了一个SCC,代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    GABOW(u):
    visit[u] <- time
    time <- time + 1
    push(S, u)
    push(P, u)
    
    for each edge (u, i) in the graph:
        if visit[i] is not defined:
            GABOW(i)
        else if i is in S:
            repeat popping nodes on P until visit[top(P)] <= visit[i]
    
    // things popped here are in the same SCC
    if top(P) == u:
        pop all node above u on S including u
        pop u from P
    

    Gabow时间复杂度也为O(V+E),常数因子的差别各位大神请自行分析。

  • 相关阅读:
    在SQL Server通过DBLINK执行ORACLE存储过程
    WIFI无线网卡全双工
    ORACLE判断日期、时间的字符串是否有效日期、时间
    FN_SPLIT-表值函数, 将字符串转列表
    EBS-从职责到报表名
    LeetCode 791 自定义字符串排序
    cgit——github快速下载器
    Ubuntu 16.04下使用git clone时报“gnutls_handshake() failed: Error in the pull function”错误
    LeetCode 1702 修改后的最大二进制字符串
    一个因编码习惯不正确而产生的BUG
  • 原文地址:https://www.cnblogs.com/wuchanming/p/4138705.html
Copyright © 2011-2022 走看看