Tarjan算法介绍

一种由Robert Tarjan提出的求解有向图强连通分量的线性时间的算法。

Tarjan与无向图连通性

·几个定义

给定无向图G=(V,E)G=(V,E)
如果割掉点x,图中的连通块数量增加,则称x为G的割点
如果割掉边e,图中的连通块数量增加,则称e为G的割边

·时间戳

在图的深度优先搜索中,按照每个节点的访问顺序所给每个点编的号,该编号叫做“时间戳”,记为dfn[x]

·搜索树

在无向连通图中任选一个节点出发进行深度优先搜索,每个点只访问一次。所有递归的边构成的一棵树称为搜索树,所有形成环的边称为返祖边

·追溯值

追溯值low[x]表示在x为根的子树内,所有边中能够到达的点的最小的dfn(不包括父亲节点)。
如果一条边(x,y)是一条返祖边,low[x]=min(low[x],dfn[x]);low[x]=min(low[x],dfn[x]);
如果一条边(x,y)是搜索树上的边,low[x]=min(low[x],low[y]);low[x]=min(low[x],low[y]);

·桥

如果一条无向边(x,y)是一条桥,一定满足dfn[x]<low[y]dfn[x]<low[y]
证明:因为low[y]>dfn[x]low[y]>dfn[x],所以在y节点的子树内一定没有一条边可以到达x或比dfn[x]还要小的点。

·割点

如果一个点x是割点,一定满足dfn[x]low[y]dfn[x]\leq low[y]
证明:因为dfn[x]low[y]dfn[x]\leq low[y],所以在以y点为根的子树内一定没有一条边可以跳到x以上,所以当x割掉,子树将独立
#无向图与双联通分量
若一张无向联通图不存在割点,则称它为点双连通图,若一张无向联通图不存在桥,则称它为边双连通图
无向图中极大的点双连通子图叫点双连通分量,极大的边双连通子图叫边双连通分量,统称为双连通分量

边双连通分量(e-DCC)的求法

把无向图中的所有桥都删去,得到的就是若干个边双。

边双缩点

在某些时候,我们需要把边双缩成一个点使得原本的连通图变成一个我们可以容易做的树,这种操作就是把一个大的边双连通分量用一个点代替。
首先我们找到low[x]=dfn[x]low[x]=dfn[x]的点,这必定是一个边双的根。
证明:对于一个边双,我们知道在这个边双内没有一条桥。既然满足low[x]=dfn[x]low[x]=dfn[x],则在x的子树内没有一个点有连边到该点的上面,所以仍在栈中的点一定是以x为根的边双。
我们可以用一个栈来储存我们经过的点,一旦这个点成为了双连通分量中的点,就可以将其弹栈。

Code:
void tarjan(int x,int fa)
{
    dfn[x]=low[x]=++idx,st[++st[0]]=x;
    for (int i=last[x];i;i=next[i])
        if (tov[i]!=fa)
            if (!dfn[tov[i]])
                tarjan(tov[i],x),low[x]=min(low[x],low[tov[i]]);
            else
                low[x]=min(low[x],dfn[tov[i]]);
    if (low[x]==dfn[x])
    {
        ++tot;
        do
            dcc[st[st[0]]]=tot;
        while (st[st[0]--]!=x);
    }
}
人工栈
void tarjan(int x){
	int index=1;f[index]=x;
	while(index){
		x=f[index];
		if(!flag[x]){
			dfn[x]=low[x]=++num;
			stack[++stack[0]]=x,bz[x]=1;
			flag[x]=1;
		}
		int i=cur[x];
		if(i){
			for(;i&&flag[tov[i]];cur[x]=i=nex[i]) 
				if(bz[tov[i]]) low[x]=min(low[x],dfn[tov[i]]);
			if(i){
				f[++index]=tov[i];cur[x]=nex[i];
				continue;
			}
		}
		if(low[x]==dfn[x]){
			bel[x]=++sz;bz[x]=0;
			while(stack[stack[0]]!=x){
				bz[stack[stack[0]]]=0;
				bel[stack[stack[0]]]=sz;
				--stack[0];
			}--stack[0];
		}
		--index;low[f[index]]=min(low[f[index]],low[f[index+1]]);
	}
}

点双连通分量(v-DCC)的求法

对于点双连通分量,需要在Tarjan里面维护一个栈,每次将当前搜索到的点加入栈中,当你发现x点满足割点条件的时候,那么仍在栈里的点就是一个以x为根的点双,把他们全部弹掉(注意:割点不要弹掉,因为一个割点有可能存在于在多个点双中)

点双的缩点

由于一个割点有可能存在于多个点双之中,所有点双的缩点不能像边双一样直接用桥边相连,我们需要新建节点来代表某个割点,用它把所有这个割点所在的点双连接起来。
对于一幅图
这里写图片描述
它拥有的点双就有
这里写图片描述
那么缩完点就是
这里写图片描述

Code:

void tarjan(int x,int fa)
{
    dfn[x]=low[x]=++idx;
    for (int i=last[x];i;i=next[i])
        if (tov[i]!=fa)
            if (!dfn[tov[i]])
            {
                st[++top]=i,tarjan(tov[i],x);
                low[x]=min(low[x],low[tov[i]]);
                if (low[tov[i]]>=dfn[x])
                {
	                cut[x]=true;
                    ++fct[x],dcc[tot][++dcc[tot][0]]=x;
                    do
                        dcc[tot][++dcc[tot][0]]=st[top];
                    while (st[top--]!=i);
                }
            }
            else low[x]=min(low[x],dfn[tov[i]]);
}
cnt=tot;
for (i=1;i<=n;++i) if(cut[i]) new_id[i]=++tot;
for (i=1;i<=cnt;++i)
	for (j=0;j<=dcc[i][0];++j)
	{
		int x=dcc[i][j];
		if(cut[x])
		{
			insert(i,new_id[x]);
			insert(new_id[x],i);
		}else c[x]=i;//除割点外,其他点仅属于1个v_DCC
	}
  • 这里有个值得注意的地方
    在缩点的时候,不能像边双一样弹栈,如果这样弹栈就会出错。
void Tarjan(int x){
	dfn[x]=low[x]=++tot;
	stack[++stack[0]]=x;
	
	for (int i=las[x];i;i=nex[i]){
		if(tov[i]==fa[x]) continue;
		if(!dfn[tov[i]]){
			fa[tov[i]]=x;
			Tanjan(tov[i]);
			low[x]=min(low[x],low[tov[i]]);
			
			if(low[tov[i]]>=dfn[x]){
				p[++p[0]]=scc[0]+1;
				
				while(stack[stack[0]]!=tov[i]){
				//注意到这里不能够直接像边双一样弹到x,否则会弹多,所以只能弹栈到当前的点
					scc[++scc[0]]=stack[stack[0]];
					stack[0]--;
				}
				scc[++scc[0]]=stack[stack[0]];
				stack[0]--;
				scc[++scc[0]]=x;
				
			}
		}
		
		else low[x]=min(low[x],dfn[tov[i]]);
	}
	
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章