有向图的连通性SCC(1)

有向图的连通性SCC(1)

之前暑假学tarjan时一直无法理解,现在有了一些浅薄的想法之后,写下一篇稚嫩的博客
以作记录。

一.先了解什么是强连通
(1)强连通:对于,u,v两点,u到v可达,并且v到u可达。那么我们称u,v强连通。
(2)强连通图:对于图G中的任意有序对(u,v)都是强连通的。
(3)强连通分量:图G中的子图满足强连通图定义。

二.那么如何去求一个图的强连通分量呢?
这里就用到了tarjan算法和Kosaraju算法。
这里先讨论tarjan算法。

我们首先要明确一点。tarjan算法是基于DFS的。DFS是可以解决无向图的连通性,以及求割点,割边的。

1.我们都知道,dfs的遍历会生成一颗dfs树。在树上的边称之为树枝边。那么不在树上的边我们称之为回退边。对于回退边有两种情况。(1)指向了祖先结点,这样的结点我们称之为后向边(2)指向了其他的树枝,这样的边我们称之为横叉边

对于这样一颗dfs树,我们不难发现如果它的一颗子树如果加上若干条回退边可以强连通那么这颗子树就是一个强连通分量。
现在我们就已经得到了一个求强连通分量的思路了。

2.我们在回过头来看看无向图求割点割边时。
在那时我们就已经引入了一个概念——时间戳dfn[]
什么是时间戳?我们将dfs的顺序称之为时间戳。也就是说,在第一次访问的时候我们需要一个数据记录访问的时间,我们记作tot。于是在每次访问时就有 dfn[u] = ++num。

3.对于u,如果它通过回退边到了v。
此时两种可能:
(1)如果是后向边那么说明它可以通过另一条路直通它的祖先v。也就是说u,v是强联通的,不过如此,我们不难发现,u,v以及其路径上的点形成了一个圈。这样就是一个强连通分量了。所以我们还需要来记录这样一个数——最早可达的祖先的时间戳。记作low[u]。初始化时,最早可以回溯的应该也是++num;
并且我们不难发现,当low[u] == dfn[u]时我们就可以得到一个强连通分量。

(2)如果是横叉边又当如何呢?其实这都是一样的。因为横叉边所指向的结点一样会更新祖先,只要他们可以形成一个圈,我们就可以认为找到了一个强连通分量。
所以,最终判定的结果还是low[u] == dfn[u]

综上,我们得到了最终判定强连通分量的结果 low[u] == dfn[u],这个u就是整个强连通分量的根(dfs树的子树的根)。

4.显然我们需要记录一个强连通分量。我们都知道,递归的过程可以用栈来模拟,所以使用一个栈来记录这样一个强连通分量。记作sta[];

这样我们就需要这些东西:
num//记录时间
dfn[],low[]//,记录时间戳以及最早回溯的祖先
stack//一个记录强连通分量的栈
e[]//图

伪代码:

tarjan(u){
       dfn[u] = low[u] = ++tot;
      stack.push(u);
      for: (u,v) in E{
          if((u,v)是树枝边){
             tarjan();
             low[u] = min(low[u],low[v])
          }
          else(回退边)
             low[u] = min(low[u],dfn[v]);
      }
      if(dfn[u] == low[u]){
         while(stack.top() != u)
          t = stack.top();
          stack.pop();
          输出t;
      }
 }

代码模板:

const int N = 1e5,M = 5e4+5;
int n,m;
//co[i] = x,表示i在第x个连通分量中
int dfn[N],low[N],sta[N],co[N];

/*邻接表存图,ver[]储存第i条边的终点
nxt储存与第i条边同起点的下一条边的储存位置
head[]表示i为起点的第一条边储存的位置
(其实只要知道任意的就行了,这里实际上存储
的是最后输入的边)*/
int head[N],ver[N*2],nxt[N*2];

int top = -1,tot = 0,num = 0,cnt = 0;
//加边,这一题是不需要边权的
void add(int x,int y){
   ver[++tot] = y;
   nxt[tot] = head[x];
   head[x] = tot;
}

void tarjan(int u){
     dfn[u] = low[u] = ++num;//初始化
     sta[++top] = u;//入栈
     for(int i = head[u];i;i = nxt[i]){
           int v = ver[i];//下一个结点
           if(!dfn[v]){//树枝边,看最早的祖先
               tarjan(v);//继续找
               low[u] = min(low[u],low[v]);
           }
           else if(!co[v])/*回退边,并且它在栈中
           这样他就有可能是强连通分量的根*/
               low[u] = min(low[u],dfn[v]);
     }
     if(low[u] == dfn[u]){//记录强连通分量
         co[u] = ++cnt;
         while(sta[top] != u){
              co[sta[top]] = cnt;//每一个节点出栈后都会被修改值
              --top;
         }
         --top;
     }
}

三,对于tarjan算法其实还有另外一种实现,这被称之为 Garbow算法

Tarjan算法里面的用dfn[maxn]和low[maxn]数组来求出强连通分量的根。Garbow在编程时是用一个堆栈来辅助求出强连通分量的根。

posted @ 2020-11-09 17:11  Paranoid5  阅读(299)  评论(0编辑  收藏  举报