tarjan算法

tarjan算法介绍

如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
图1中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
Tarjan算法是用来求有向图的强连通分量的。求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
在这里插入图片描述
算法演示:
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

至此,算法结束。经过该算法,求出了图2中全部的三个强连通分量{1,3,4,2},{5},{6}。

可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)

一、tarjan算法求强连通分量

代码:

void tarjan(int u){
    //求强连通分量
    dfn[u]=low[u]=++cnt;
    sk.push(u);
    instack[u]=1;
    for(auto v:G[u]){
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(instack[v])
            low[u]=min(low[u],dfn[v]);//求强连通分量时可以写成low[u]=min(low[u],low[v]);,但求割点时不行
    }
    if(low[u]==dfn[u]){
        ++Bcnt;//Bcnt为每个强连通分量的编号
        int x;
        do{
            x=sk.top();
            scc[x]=Bcnt;
            instack[x]=0;
            sk.pop();
        }while(u!=x);

    }
}

二、tarjan缩点

求出强连通分量后,以每个分量的scc编号为缩点后的顶点编号建立新图
代码

#include<iostream>
#include<algorithm>
#include<queue>
#include<set>
#include<stdlib.h>
#include<time.h>
#include<unordered_map>
#include<stack>
#include<cstdio>
#include<vector>
#include<cstring>
#include<string>
#include<map>
#include<cmath>
#include<bitset>
#define ll long long
using namespace std;
typedef pair<int,int> P;
const int N=1e5+10;
const ll mod=1e9+7;

int dfn[N],low[N],scc[N],cnt,Bcnt;
bool instack[N],vis[N];
int w[N],n,m,ans;
stack<int> sk;
vector<int> G[N],dag[N];//dag用于存储缩点后新图的边

void tarjan(int u){
    dfn[u]=low[u]=++cnt;
    sk.push(u);
    instack[u]=1;
    for(auto v:G[u]){
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(instack[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]){
        ++Bcnt;//Bcnt为每个强连通分量的编号
        int x;
        do{
            x=sk.top();
            scc[x]=Bcnt;
            instack[x]=0;
            sk.pop();
        }while(u!=x);

    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>w[i];
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        G[u].push_back(v);
    }
    for(int i=1;i<=n;i++){
        if(!dfn[i]){ //如果i节点没有遍历过,
            tarjan(i);
        }
    }
    for(int i=1;i<=n;i++){//建立新图
        for(auto v:G[i]){
            if(scc[v]!=scc[i]){
                dag[scc[i]].push_back(scc[v]);
                ind[scc[v]]++;//入度加1
            }
        }
    }
    return 0;
}

/*
6 8
1 2 3 2 1 1
4 5
5 6
6 4
1 2
2 3
3 1
6 2
4 1
*/

三、tarjan求割点、桥

求割点

u为割点应满足的条件:
(1) u为树根,且u有多于一个子树。
(2) u不为树根,且存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得dfn(u)<=low(v).也就是u的子树中的v点无法到达u之前的点,所以u点去掉就是两个连通分支,所以u为割点

代码

#include<iostream>
#include<algorithm>
#include<unordered_map>
#include<stack>
#include<cstdio>
#include<vector>
#include<cstring>
#include<string>
#include<map>
#define ll long long

using namespace std;
typedef pair<int,int> P;
const int N=1e5+10;
const ll mod=1e9+7;

int dfn[N],low[N],scc[N],cnt,Bcnt;
map<int,int> mp;
bool instack[N],vis[N];
bool iscp[N];//割点
bool isce[N];//割边
int w[N],n,m,ans,tmp,sum[N],root,cpnum;
stack<int> sk;
vector<int> G[N],dag[N];
void tarjan(int u){
    //求割点
    dfn[u]=low[u]=++cnt;
    int col=0;//计数子树个数
    for(auto v:G[u]){
        if(!dfn[v]){
            col++;
            tarjan(v);
            low[u]=min(low[u],low[v]);
            if((u==root&&col>1)||u!=root&&low[v]>=dfn[u])//注意不能再里面统计割点数量,因为同一个点可能会多次统计
                iscp[u]=1;//判断割点的两个条件
        }
        else low[u]=min(low[u],dfn[v]);
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;
        cin>>x>>y;
        G[x].push_back(y);
        G[y].push_back(x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]){
            root=i;
            tarjan(i);
        }

    for(int i=1;i<=n;i++)//必须要再tarjan函数外统计割点数量
        if(iscp[i])
            cpnum++;

    printf("%d\n",cpnum);
    for(int i=1;i<=n;i++){
        if(iscp[i])
            printf("%d ",i);
    }
    printf("\n");
    return 0;
}

求桥(割边)

一条边(u,v)是桥,当且仅当(u,v)为树枝边(即非负边),且满足dfn(u)<low(v)(前提是其没有重边)。

也就是,u的儿子v之间只有一条边(前提是无重边),且v点只能到u点到不了u点前,所以(u,v)边去掉就是两个连通分支,所以(u,v)为桥

注意:找桥的时候,要注意看有没有重边。有重边,则不是桥。

代码

void tarjan(int u){
    //求割点
    dfn[u]=low[u]=++cnt;
    int col=0;//计数子树个数
    for(auto v:G[u]){
        if(!dfn[v]){
            col++;
            tarjan(v);
            low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v]&&mp[v][u]<=1&&mp[u][v])//mp用于判断是否有重边
                isce[v]=1;//判断割边的条件
        }
        else low[u]=min(low[u],dfn[v]);//求割点割边时不能写成low[u]=min(low[u],low[v]);
    }
}

还可用链式前向星存图,方便处理重边:

#include<iostream>
#include<algorithm>
#include<queue>
#include<set>
#include<stdlib.h>
#include<time.h>
#include<unordered_map>
#include<stack>
#include<cstdio>
#include<vector>
#include<cstring>
#include<string>
#include<map>
#include<cmath>
#include<bitset>
#define ll long long
using namespace std;
typedef pair<int,int> P;
const int N=1e5+10;
const ll mod=1e9+7;

int dfn[N],low[N],scc[N],cnt,Bcnt;
map<int,int> mp[N];
bool instack[N];
bool isce[N];//割边
int n,m,ans;
int ver[N*4],head[N],tot,nextv[N*4];

void addedge(int x,int y){
    //链式前向星存图
    ver[++tot]=y,nextv[tot]=head[x],head[x]=tot;
}
void tarjan(int u,int in_edge){
    //求桥
    dfn[u]=low[u]=++cnt;
    for(auto i=head[u];i;i=nextv[i]){
        int v=ver[i];
        if(!dfn[v]){
            tarjan(v,i);
            low[u]=min(low[u],low[v]);

            if(low[v]==dfn[v]&&!isce[v]){
                isce[i]=isce[i^1]=1;
            }
        }
        else if(i!=(in_edge^1))//如果有重边,通过另一条边到达u节点的编号不可能等于in_edge^1
            low[u]=min(low[u],dfn[v]);//求割点割边时不能写成low[u]=min(low[u],low[v]);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值