tarjan原理
tarjan是图论中常用的算法,用于求图中的强连通分量,割点等
这里先讲强连通分量的做法
强连通分量的定义:
在一个有向图的子图中,任意两个点相互可达,也就是存在互通的路径,那么这个子图就是强连通分量(或者称为强连通分支)。如果一个有向图的任意两个点相互可达,那么这个图就称为强连通图。
tarjan基于dfs。而且每个强连通分量恰好是深搜树的一颗子树。
如果u是某个强连通分量的根,那么:
(1)u不存在路径可以返回到它的祖先
(2)u的子树也不存在路径可以返回到u的祖先。
DFN[i]标记i这个点被访问到的时间
low[i]表示i这个点直接或者间接可以到达的点里面最早被访问到的点的时间(实际上就是同一个强连通分量里的根)
步骤:
从第1个点(u)开始搜索,刚开始要把DFN[u]和low[u]赋值为被访问到的时间, 每次都去遍历这个点关联的边<u,v>:
如果v不在栈里面,继续去递归搜索v,等到回溯以后,就要对low[u]判断,如果u的子树能到达更早的点,那么就把low[u]赋值为low[v[i]]。
如果v已经在栈里面了,此时形成一个环,那么此时对于low[u]的修改就是:
这样很明显,只有强连通分量的根的DFN值和low值是相等的,其他的点都进行过修改,所以如果DFN[i]==low[i],说明我们找到了一个强连通分量的根,要得到这个强连通分量,只要把相应元素出栈就行了。
由于每个点只访问1次,每条边也是1次,所以tarjan算法的时间复杂度是O(n+m)。
代码:
#include<stdio>
#include<cstring>
const int maxn=10005;
int DFN[maxn];//记录每个点被访问到的时间
int low[maxn];//记录点可以直接或间接到达的最早被访问到的点(也就是那个强连通分量的根)
int stack[maxn];
int sccnum[maxn];//标记每个点属于第几个强连通分量
bool instack[maxn];
int sccNum;//强连通分量的数目
int top;
int index;
int n;
struct node
{
int to;
int next;
}edge[10*maxn];
int head[maxn];
int tot;
void addedge(int from,int to)
{
edge[tot].to=to;
edge[tot].next=head[from];
head[from]=tot++;
}
void tarjan(int i)
{
DFN[i]=low[i]=++index;//刚刚搜到这个点,DFN和low都赋值为被访问到的时间
stack[top++]=i;//入栈
instack[i]=1;
for(int j=head[i];j!=-1;j=edge[j].next)
{
if(!DFN[edge[j].to])//如果没有被访问过
{
tarjan(edge[j].to);
//这个时候low可能要修改,值为i或者i的子树可以到达的最早被访问到的点的时间
if(low[i]>low[edge[j].to])
low[i]=low[edge[j].to];
}
else if(instack[edge[j].to])//已经在栈
{
if(low[i]>DFN[edge[j].to])
low[i]=DFN[edge[j].to];
}
}
if(DFN[i]==low[i])//找到根
{
sccNum++;
int v;
do
{
v=stack[--top];
sccnum[v]=sccNum;
instack[v]=0;//标记出栈
}while(v!=i);
}
}
void solve()
{
memset(DFN,0,sizeof(DFN));
memset(instack,0,sizeof(instack));
index=0;
sccNum=0;
top=0;
for(int i=1;i<=n;i++)
if(!DFN[i])
tarjan(i);
}
int main()
{
int m,a,b;
while(~scanf("%d%d",&n,&m))
{
if(n==0 && m==0)
break;
memset(head,-1,sizeof(head));
tot=0;
for(int i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
addedge(a,b);
}
solve();
if(sccNum==1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
基于强联通分量,还可以扩展出一些其他的算法:
1. 割点:
定义:在一张无向图中,如果去掉某个顶点以及和这个顶点相关联的边,使得整个图的连通分支数增 加,那么这个点就是一个割点.
tarjan求割点:
若一个节点为割点,一定满足以下两个条件之一:
1).u是dfs搜索树的根,并且u含2棵及2棵以上的子树,即不同子树的节点要想联通必须经过u,那么把u删去图中的连通分支一定增加。
2).u不是dfs搜索树的根,并且有不等式 low[v]>=DFN[u],其中(u,v)是树枝边(即v通过u延伸开去),因为v一定不会到达时间比u小的点,否则low[v]一定会小于dfn[u],所以v要达到祖先一定要经过u。
代码:
2.桥:
定义:
在一张无向图中,如果去掉边(u,v)使得图的连通分支数增加,那么边(u,v)便称为桥.
类似于割点,只不过是把点变为一条边而已。边(u,v)是无向图的桥当且仅当(u,v)满足 low[v]>DFN[u],另外要对于u,v间有重边进行特判。
代码:
void tarjan(int u,int root,int fa)
{
DFN[u]=low[u]=++index;
instack[u]=1;
int cnt=0;
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(!instack[v])
{
tarjan(v,root,u);
cnt++;
if(low[u]>low[v])
low[u]=low[v];
if(u==root && cnt>1)
cut_point[u]=1;
else if(u!=root && low[v]>=DFN[u])
cut_point[u]=1;
}
else if(v!=fa && low[u] > DFN[v])
low[u]=DFN[v];
}
3.双联通分量:
在无向连通图中,如果删除该图的任何一个结点都不能改变该图的连通性,则该图为双连通的无向图。一个连通的无向图是双连通的,当且仅当它没有关键点。换言之,双连通分量里任何2个顶点之间都至少有2条不相交的路径
求法类似于求强联通分量,只不过是在无向图中。
代码:
void tarjan(int u,int fa)
{
instack[u]=1;
DFN[u]=low[u]=++index;
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(fa==edge[i].id)
continue;
if(!instack[v])
{
tarjan(v,edge[i].id);
low[u]=min(low[u],low[v]);
}
else
low[u]=min(DFN[v],low[u]);
}
}