tarjan算法是在有向图中求强联通分量的一种算法,基于dfs
其中最重要的需要维护的两个数组是low[maxn],dfn[maxn]
low[u]代表u可以到达的最近节点,dfn[u]代表u在dfs树中的深度
其原理是
1.若一个点u是强联通分量的根节点,那么这个点在dfs中的遍历顺序dfn[u]的大小一定会等于它可以到达的最小节点的遍历顺序low[u],即这个点最短只能自己到自己
2.若一个点u不是强联通分量的根节点,那么它可以到达的最小节点的遍历顺序low[u]一定小于他自身的遍历顺序大小dfn[u]
换句话说,在dfs到一个强联通分量时,第一个被dfs到的点的dfs序一定比这个强联通分量里的其他点的dfs序小,于是我们就可以把这个强连通分量看作一棵树,用染色的方式来将它们全部找出来
注意两个概念:
1.这里讲到的遍历顺序是指用dfs以某个确定节点为起点遍历整个图时访问到某个节点的顺序,如根节点的dfn为1,根节点下一层的节点的dfn都为2
2.上面提到的近是用dfn来衡量的,即dfn越小,点越近
好了,明白了原理之后,算法框架就可以构建了
假设要找点u属于的强联通分量中有哪些点
1.管你怎么办,反正首先存一个图
2.然后以u为起点,将u放入一个栈中,标记u在栈中
3.对于u的每一个子节点v构成的边(u,v),都有三种情况:要么是一条树边,要么是一条返祖边,要么是一条横叉边,故我们可以对树边和返祖边进行处理,而横叉边不用考虑(若在同一个分量中就不可能构成横叉边了),于是对于树边(特征是没有被访问过),就先dfs再用low[u]和low[v]的较小值来更新low[u],对于返祖边(特征是在栈中),就用low[u]和dfn[v]的较小值来更新low[u]
4.如果u是根节点,即dfn[u]==low[u],那就意味着栈中u上面的节点都是u的联通点,于是将u上面的点都弹出来放在数组中就可以得到u的强连通分量点了
相关代码如下
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX 100010
int dfsnum[MAX],dfsNum,low[MAX];
int sccnum[MAX],sccNum;
int instack[MAX],st[MAX],top;
typedef struct EDGE
{
int v,next;
}edge;
edge e[MAX];
int edgeNum;
int head[MAX];
void insertEdge(int a,int b)
{
e[edgeNum].v=b;
e[edgeNum].next=head[a];
head[a]=edgeNum++;
}
void Tarjan(int i)//找i的强联通分量
{
dfsnum[i]=low[i]=++dfsNum;//时间戳,最近访问节点为dfsNum
st[top++]=i;//入栈
instack[i]=1;//标记入栈
int j=head[i];
for(j=head[i];j!=-1;j=e[j].next)//遍历i的子节点
{
int v=e[j].v;//v是i的子节点
if(dfsnum[v]==0)//为树边,因为v没有经过dfs
{
Tarjan(v);//dfs v
if(low[i]>low[v])//其实就是更新low值,最近访问节点值
low[i]=low[v];//可改成low[i]=min(low[i],low[v]);
}
else if(instack[v])//如果是反祖边的话,就意味着i的low值可能会变小
{
if(low[i]>dfsnum[v])
low[i]=dfsnum[v];//low[i]=min(low[i],dfsnum[v]);
}
}
if(dfsnum[i]==low[i])//如果i是根节点,马上吐栈
{
do
{
top--;
sccnum[st[top]]=sccNum;
instack[st[top]]=0;
}while(top>=0&&st[top]!=i);
sccNum++;
}
}
void solve(int n)//n有用?
{
int i;
memset(dfsnum,0,sizeof(dfsnum));
memset(instack,0,sizeof(instack));
dfsNum=0;
top=0;
sccNum=0;
for(i=1;i<=n;i++)
{
if(dfsnum[i]==0)//如果没有进入分量图,tarjan(i),找i的强联通分量
Tarjan(i);
}
}
int main()
{
int n,m;
int a,b,i;
while(scanf("%d %d",&n,&m))
{
if(m==0&&n==0)
break;
memset(head,-1,sizeof(head));
edgeNum=0;
for(i=0;i<m;i++)
{
scanf("%d %d",&a,&b);
insertEdge(a,b);//插边
}
solve(n);
if(sccNum==1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
tarjan这个人很厉害,他发明了3个算法,分别可以求强联通分量,桥和割点
断点
若点u是断点,显然它后面的点的low数组都比割点的low数组大
桥
若low[v]>dfn[u],则(u,v)为割边。但是实际处理时我们并不这样判断,因为有的图上可能有重边,这样不好处理。我们记录每条边的标号(一条无向边拆成的两条有向边标号相同),记录每个点的父亲到它的边的标号,如果边(u,v)是v的父亲边,就不能用dfn[u]更新low[v]。这样如果遍历完v的所有子节点后,发现low[v]=dfn[v],说明u的父亲边(u,v)为割边。
给道题吧:
天凯是苏联的总书记。苏联有n个城市,某些城市之间修筑了公路。任意两个城市都可以通过公路直接或者间接到达。
天凯发现有些公路被毁坏之后会造成某两个城市之间无法互相通过公路到达。这样的公路就被称为dangerous pavement。
为了防止美帝国对dangerous pavement进行轰炸,造成某些城市的地面运输中断,天凯决定在所有的dangerous pavement驻扎重兵。可是到底哪些是dangerous pavement呢?你的任务就是找出所有这样的公路。
Input format:
第一行n,m(1<=n<=150, 1<=m<=5000),分别表示有n个城市,总共m条公路。
以下m行每行两个整数a, b,表示城市a和城市b之间修筑了直接的公路。
Output format:
输出有若干行。每行包含两个数字a,b(a<b),表示<a,b>是dangerous pavement。请注意:输出时,所有的数对<a,b>必须按照a从小到大排序输出;如果a相同,则根据b从小到大排序。
Sample:
6 6
1 2
2 3
2 4
3 5
4 5
5 6
Dager.out
1 2
5 6
code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=150+5;
const int maxm=5000+5;
bool G[maxn][maxn];
int low[maxn],dfn[maxn],DFN=0;
int n,m;
int numofgroup=0,pointer=0;
struct node{
int u;
int v;
};
node ans[maxm];
void datasetting()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)memset(G[i],0,sizeof(G[i]));
int u,v;
for(int i=0;i<m;i++)
{
scanf("%d%d",&u,&v);
G[u][v]=G[v][u]=true;
}
}
void tarjan(int u,int fa)
{
low[u]=dfn[u]=++DFN;
for(int v=1;v<=n;v++)
{
if(G[u][v])
{
if(!dfn[v])
{
tarjan(v,u);
low[u]=min(low[v],low[u]);
if(dfn[u]<low[v])
{
node a;
a.u=u;a.v=v;
ans[pointer++]=a;
}
}
else if(v!=fa)low[u]=min(low[u],dfn[v]); }
}
}
bool cmp(node a,node b)
{
if(a.u==b.u)return a.v<b.v;
return a.u<b.u;
}
int main()
{
datasetting();
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,i);
sort(ans,ans+pointer,cmp);
for(int i=0;i<pointer;i++)
{
node a=ans[i];
printf("%d %d\n",a.u,a.v);
}
return 0;
}