tarjan算法求强连通分量

前置知识

强连通分量

有向图强 连通分量 :在 有向图 G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点 强连通,如果有向图G的每两个顶点都强连通,称G是一个 强连通图 。有向图的极大强连通子图 ,称为强连通分量。

——————baidu

存图

可以用前向星也可以邻接矩阵

正题

可以结合最后的代码理解一下内容

tarjan算法是用来求有向图中的强连通分量的

至于一张有向图它会长以上这样

关于算法中会有两个数组\(dfn[i]\)\(low[i]\)

\(dfn[i]\)姑且可以当作一种标记,标记它是否访问过,它的值是由dfs的顺序产生的,在上面的图中可以当作和标号相同

\(low[i]\)你可以叫它时间戳,很烦,所以就叫它low吧,它在操作过程中是在变化的

我们用\((i,j)\)表示\((dfn[i],low[i])\)

如上图,当我们都访问一遍,仅仅只是初始化,就是下面这张图

先初始化一个\(tot=0\),每次访问到新的节点是就\(++\),用tot来更新low和dfn

递归访问

我们从1号点开始进行递归

此时 \(tot=1\)并且\(dfn=0\)

先入栈

1号点的low和dfn被更新为1,即表示为\((1,1)\)

继续递归到二号点

此时 \(tot=2\)并且\(dfn=0\)

先入栈

2号点的low和dfn被更新为2,即表示为\((2,2)\)

继续递归到三号点

此时 \(tot=3\)并且\(dfn=0\)

先入栈

3号点的low和dfn被更新为3,即表示为\((3,3)\)

继续递归到四号点

此时 \(tot=4\)并且\(dfn=0\)

先入栈

4号点的low和dfn被更新为4,即表示为\((4,4)\)

随后从四号递归到五号点

此时 \(tot=5\)并且\(dfn=0\)

先入栈

5号点的low和dfn被更新为5,即表示为\((5,5)\)

到这里我们发现五号点没有下一个点了,所以不会继续访问下一个点,即开始对五号点的操作

我们会发现,5号点并没有一条指向其它点的边

所以它会直接进行下一步操作,判断dfn与low是否相等

很显然\(5==5\)所以5这个点是一个单独的强连通分量,再出栈

此时栈中元素有:4,3,2,1

五这个点的递归结束,回到四这个点,对四进行操作

首先5号已经为一个单独的强连通分量了所以不管它

继续遍历,发现2号点的dfn不为0,所以它不会进行递归,而是判断下一步,判断是否在栈中,在就更新,不在就不管了,2号是在栈中的,并且2号的low是远小于4号点的low,所以4号点的low被更新为2,即(4,2)

注:这个判断下一步一般会在这个点有多条边指向其他点,然后这几个其它点中有一个把这个点给访问了,所以就没法递归访问,只能试试在不在栈中能不能更新了

low[4]和dfn[4]不相等

至此四号点遍历结束,回到三号点

三号点之前是对四号点进行访问的4号点,所以会先对4号点进行操作,4号的low是小于3号点的low,所以3号点的low被更新为2,即(3,2)

low[3]与dfn[3]不相等

随后三号点会遍历到五号点并试图递归到五号

发现五号的\(dfn\)不为\(0\)​​,所以它不会进行递归,而是判断下一步,判断它是否在栈中,很显然5号点在之前就已经出栈了,所以什么都不做

注:见上上

low[3]和dfn[3]依然不相等

三号遍历结束,回到二号

2号只能遍历到3号,而2号的low和3号的low大小相同,所以不会被更新,还是(2,2)

二号遍历结束,发现二号的low和dfn相等

这时图长这样

栈中元素还是:4,3,2,1

我们已经判断到二号点的low和dfn相等且无法被更新了

所以就会有一个以二号点为开头的强连通分量

栈从4弹出到2,

此时4,3,2就是一个强连通分量

再回到一号点

2号点无法更新1号点

low[1]与dfn[1]相等

所以从1号点开始出栈,出到1号,一号作为一个强连通分量

此时栈中空

至此tarjan算法结束

强连通分量有5 4,3,2 1 共三个

最后附上代码以供理解

#include<bits/stdc++.h>
using namespace std;
const int M=1000001;
int read() {
    int x(0);bool f(0);char c(getchar());
    while(c<'0'||c>'9') {f^=c=='-';c=getchar();}
    while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    return f?(~x)+1:x;
}
int dfn[M],low[M],head[M],num,s[M],tot,res,ans[M],out;
stack<int>g;
struct op {
    int nxt,to,dis;
}e[M];
void add(int frm,int to) {
    e[++num].nxt=head[frm];
    e[num].to=to;
    head[frm]=num;    
}
void tarjan(int u) {
    low[u]=dfn[u]=++tot;//访问时的更新
    g.push(u);//入栈(只要在栈中就说明还没有被列为强连通分量)
    s[u]=1;//标记已经在栈中了
    for(int i=head[u];i;i=e[i].nxt) {//遍历这个点所有指向其它点的有向边
        int v=e[i].to;//其实遍历边就相当于遍历这个点所指向的其它的点
        if(dfn[v]==0) {//如果这个点之前没有访问过
            tarjan(v);//访问这个点
            low[u]=min(low[u],low[v]);//访问完这个点之后的所有点后,回来后对这个点进行更新
        }
        else if(s[v]) {//这个点访问过了(这个点之前的点把这个点给访问了),并且它还在栈中(就是还没有被列为一个强连通分量)
            low[u]=min(low[u],low[v]);//对这个点进行更新
        }
    }
    //这个点所有指向其它点的有向边(所指向的其它的点)遍历完了
    //若进行了下面一步,说明找到了一个强连通分量
    if(low[u]==dfn[u]) {//判断到这个点的low和dfn是相等的(也就是说这个点是一个强连通分量的起点)
        while(g.top()!=u) {//栈中后入的元素是在上面的,并且这个点后面(递归返回的时候是倒着的)的强连通分量已经被找到并弹出来,所以不必担心会有其它点的影响。
            //所以我们就一直弹,弹到当前这个点为止,弹出的这些点就是一组强连通分量
            int w=g.top();
            g.pop();//弹出元素
            s[w]=0;//标记它已经不在栈中了
        }
        //这个点自身也是这个强连通分量中的一个,所以也要弹出标记
        g.pop();
        s[u]=0;
        //因为写stack的话感觉会好理解点,如果你要手写栈,就可以用do{}while;来避免这个点自己出不了栈了
    }
}
int main() {
    int n=read(),m=read();
    for(int i=1;i<=m;++i) {
        int u=read(),v=read();
        add(u,v);
    }
    for(int i=1;i<=n;++i) {
        if(!dfn[i])tarjan(i);
    }
    return 0;
}

无注释版本

#include<bits/stdc++.h>
using namespace std;
const int M=1000001;
int read() {
    int x(0);bool f(0);char c(getchar());
    while(c<'0'||c>'9') {f^=c=='-';c=getchar();}
    while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    return f?(~x)+1:x;
}
int dfn[M],low[M],head[M],num,s[M],tot,res,ans[M],out;
stack<int>g;
struct op {
    int nxt,to,dis;
}e[M];
void add(int frm,int to) {
    e[++num].nxt=head[frm];
    e[num].to=to;
    head[frm]=num;    
}
void tarjan(int u) {
    low[u]=dfn[u]=++tot;
    g.push(u);
    s[u]=1;
    for(int i=head[u];i;i=e[i].nxt) {
        int v=e[i].to;
        if(dfn[v]==0) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(s[v]) {
            low[u]=min(low[u],low[v]);
        }
    }
    if(low[u]==dfn[u]) {
        while(g.top()!=u) {
            int w=g.top();
            g.pop();
            s[w]=0;
        }
        g.pop();
        s[u]=0;
    }
}
int main() {
    int n=read(),m=read();
    for(int i=1;i<=m;++i) {
        int u=read(),v=read();
        add(u,v);
    }
    for(int i=1;i<=n;++i) {
        if(!dfn[i])tarjan(i);
    }
    return 0;
}
posted @ 2021-12-27 22:30  双枫  阅读(66)  评论(4编辑  收藏  举报