1. 程式人生 > >Tarjan無向圖縮環(求邊雙)/有向圖縮環(求邊雙)/無向圖求點雙

Tarjan無向圖縮環(求邊雙)/有向圖縮環(求邊雙)/無向圖求點雙

邊雙與點雙

不嚴謹的定義,
邊雙=刪掉一條邊依然連通
點雙=刪掉一個點依然連通

無向圖Tarjan求邊雙

先說無向圖。無向圖就比有向圖簡單一些,因為只有返祖邊而沒有橫叉邊。
用棧來儲存已訪問的點。

  1. 如果已經訪問過了,就把它當返祖邊處理(low=min(low,dfn[to]))(low=min(low,dfn[to]))
  2. 若沒有訪問過則繼續搞,low=min(low,low[to])low=min(low,low[to])
    最後,當遍歷完所有兒子後,若當前點x的dfn=low (low初值為dfn),則開始退棧,直至退出x。
    此時退出的點構成強連通分量。

有向圖Tarjan求邊雙

用棧來儲存已訪問的點。
與無向圖略有不同的是,你需要判斷出邊連向的點是否已訪問過

  1. 如果已經訪問過了,那麼是否還在棧中,在棧中 (不一定是x的祖先) 就把它當返祖邊處理(low=min(low,dfn[to]))(low=min(low,dfn[to]))。不在棧中就無視它。
  2. 若沒有訪問過則繼續搞,low=min(low,low[to])low=min(low,low[to])

當前點x的dfn=low時 (low初值為dfn),開始退棧,直至退出x。
此時退出的點構成強連通分量。
來自hiho的虛擬碼

tarjan(u)
{
    Dfn[u]=Low[u]=++Index                      // 為節點u設定次序編號和Low初值
    Stack.push(u)                              // 將節點u壓入棧中
    for each (u, v) in E                       // 列舉每一條邊
        if (v is not visted)                   // 如果節點v未被訪問過
            tarjan(v)                          // 繼續向下找
Low[u] = min(Low[u], Low[v]) else if (v in Stack) // 如果節點v還在棧內(很重要,無向圖沒有這一步) Low[u] = min(Low[u], Dfn[v]) if (Dfn[u] == Low[u]) // 如果節點u是強連通分量的根 repeat v = Stack.pop // 將v退棧,為該強連通分量中一個頂點 mark v // 標記v,同樣通過棧來找連通分量 until (u == v) }

一些理解

無向圖的很好理解,對於有向圖:

首先,在dfs搜尋樹中,每個強連通分量都是其中深度最小節點為根向下的一個連通塊。

這個棧的前身是一個dfs序,但是出掉了一些點。

這個棧裡面每一個x都可以到達任何一個在他後面的點。low[x]其實就是一個在棧中最前能到達的位置。(通過這個定義也可以知道其實對於返祖邊low可以直接取祖先節點的low更新,而不用取dfn)

考慮一個點什麼時候被歸屬為一個環中(出棧)。
當退掉一個點x的時候low[x]一定等於當前點dfn?
看這組資料(8有向邊),從1開始縮

1 2
2 3
3 4
4 5
5 3
3 6
6 7
7 1

會發現low有一種類似並查集的“繼承關係”,若low[x] = y,low[y] = z,那麼相當於low[x] = z,也就是x在z的時候才會退掉。所以在x退掉的點實際上low就是x,也就是最前可達x,再綜合上棧內點可達在此之後的點,這實質上就是一個環了。

利於實現的細節

  1. 可以先標記是屬於哪個環的,之後再統一操作
  2. 將環中的所有點的邊 集中 可以用邊集陣列指標指指指來做

無向圖Tarjan求點雙

(建圓方樹,一個點雙內所有點向一個新方點連邊,並去除其他邊)
與邊雙不同的是,一個點可以在多個點雙內。所以縮的時候有點不一樣。

分三種情況:
當子樹內最多隻能走到當前點時,當前點就是這個點雙的最高點。(此時縮點)
當子樹內能走到比當前點還高時,說明當前點被更高的點雙包括了。
當子樹內無法走到當前點時,說明這是一條正常的邊。

void tarjan(int x,int from) {
	dfn[x]=low[x]=++stm;
	S[++S[0]]=x;
	for (int i = gfinal[x]; i; i=gnex[i]){
		if (i==(from^1)) continue;
		int y = gto[i];
		if (dfn[y]==0) {
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if (low[y] > dfn[x]) {
				link(x,y),link(y,x);
				S[0]--;
			} else if (low[y]==dfn[x]) {
				dcnt++;
				while (S[S[0]+1] != y) { //注意是縮到y,而不是縮到x。反例就是兩個三角環,第二個接在第一個的某一角。
					link(S[S[0]],dcnt);
					link(dcnt,S[S[0]]);
					S[0]--;
				}
				//x是保留在棧內等待其父親將他出掉的。
				link(x,dcnt),link(dcnt,x);
			}
		} else low[x]=min(low[x],dfn[y]);
	}
}

(不知道哪來的)Kosaraju

時間同樣為O(n),但常數比Tarjan大.

首先我們在圖GG的逆向圖中進行一次dfs求出後序序列(出點時標號).
然後在原圖中按照所求序列,逆序對未被dfs到的點進行dfs遍歷,每一次得到的一棵DFS樹就是一個強連通分量. 即需要dfs2次.

證明

記在第二次DFS中, 當前樹根為RR,當前遍歷到的點為CC

求證: R與C互達

已知C的後序編號肯定比R的要小,且在原圖中,存在路徑(R,C),就意味著在逆向圖中存在路徑(C,R)
現在需要證明的就是,在原圖中存在路徑(C,R),即在逆向圖中存在路徑(R,C).

因為R編號比C要大,那麼假如在逆向圖中不存在路徑(R,C)
但因為已知逆向圖中存在路徑(C,R),那麼R的編號必然比C要小1.這與已知矛盾.

所以逆向圖中必然存在路徑(R,C),即原圖中存在路徑(C,R).

即R與C是互達的.對於每一個C都可以這樣考慮,然後根據傳遞性得出,這是一個強連通分量.

第二個方面: 會不會有點存在於該強連通分量,而沒有被第二次DFS到呢?
因為強連通,所以必然會DFS到.

參考

  1. 若不經過C到了R,那麼R顯然會比C早退出.
  2. 若經過C到了R,顯然R也會比C早退出.
  3. 若在逆向圖中存在先到R,且在R沒退出的情況下到了C,與已知不矛盾,且說明了在逆向圖中存在路徑(R,C).
  1. 3種情況考慮: 當存在路徑(C,R)時, ↩︎