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]]);
	}
	
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章