Tarjan無向圖縮環(求邊雙)/有向圖縮環(求邊雙)/無向圖求點雙
邊雙與點雙
不嚴謹的定義,
邊雙=刪掉一條邊依然連通
點雙=刪掉一個點依然連通
無向圖Tarjan求邊雙
先說無向圖。無向圖就比有向圖簡單一些,因為只有返祖邊而沒有橫叉邊。
用棧來儲存已訪問的點。
- 如果已經訪問過了,就把它當返祖邊處理。
- 若沒有訪問過則繼續搞,
最後,當遍歷完所有兒子後,若當前點x的dfn=low (low初值為dfn),則開始退棧,直至退出x。
此時退出的點構成強連通分量。
有向圖Tarjan求邊雙
用棧來儲存已訪問的點。
與無向圖略有不同的是,你需要判斷出邊連向的點是否已訪問過
- 如果已經訪問過了,那麼是否還在棧中,在棧中 (不一定是x的祖先) 就把它當返祖邊處理。不在棧中就無視它。
- 若沒有訪問過則繼續搞,
當前點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,再綜合上棧內點可達在此之後的點,這實質上就是一個環了。
利於實現的細節
- 可以先標記是屬於哪個環的,之後再統一操作
- 將環中的所有點的邊 集中 可以用邊集陣列指標指指指來做
無向圖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大.
首先我們在圖的逆向圖中進行一次dfs求出後序序列(出點時標號).
然後在原圖中按照所求序列,逆序對未被dfs到的點進行dfs遍歷,每一次得到的一棵DFS樹就是一個強連通分量. 即需要dfs2次.
證明
記在第二次DFS中, 當前樹根為,當前遍歷到的點為
求證: 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到.
參考
- 若不經過C到了R,那麼R顯然會比C早退出.
- 若經過C到了R,顯然R也會比C早退出.
- 若在逆向圖中存在先到R,且在R沒退出的情況下到了C,與已知不矛盾,且說明了在逆向圖中存在路徑(R,C).
3種情況考慮: 當存在路徑(C,R)時, ↩︎