一叶浮萍归大海

人生何处不相逢

Tarjan算法介绍

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

Tarjan与无向图连通性

·几个定义

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

·时间戳

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

·搜索树

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

·追溯值

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

·桥

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

·割点

如果一个点x是割点,一定满足$$dfn[x]\leq low[y]$$
证明:因为\(dfn[x]\leq low[y]\),所以在以y点为根的子树内一定没有一条边可以跳到x以上,所以当x割掉,子树将独立

无向图与双联通分量

若一张无向联通图不存在割点,则称它为点双连通图,若一张无向联通图不存在桥,则称它为边双连通图
无向图中极大的点双连通子图叫点双连通分量,极大的边双连通子图叫边双连通分量,统称为双连通分量

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

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

边双缩点

在某些时候,我们需要把边双缩成一个点使得原本的连通图变成一个我们可以容易做的树,这种操作就是把一个大的边双连通分量用一个点代替。
首先我们找到\(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
	}
posted @ 2019-08-10 20:02  Chandery  阅读(375)  评论(0编辑  收藏  举报