強連通分量的三個求法

2021-09-06 19:56:21 字數 3429 閱讀 6786

這裡主要談及強連通分量(以下簡稱scc,strongly connected component)三種常見的求法(以下涉及的圖均為有向圖),即kosaraju、tarjan和gabow。三種演算法背後的基礎思想都是dfs,只是它們通過dfs獲得了不同的資訊。各位大哥大姐繼續往下讀之前,最好對dfs相關的概念和性質比較熟悉,例如,什麼叫做search" target="blank">反向邊、交叉邊。

我就不八卦這大哥了。kosaraju的方法也就是導論第二版中文22章中講的方法, 即廣為人知的兩遍dfs。kosaraju演算法說白了就是先對原圖來一遍dfs, 再把所有邊的方向都倒過來,按照剛才dfs求出的結點完成時間的逆序,再來一遍dfs。

那麼為啥可以這麼做呢?

這就需要乙個性質來幫忙,即當a、b為兩個scc,且存在有向邊(a, b),其中a∈a,b∈b, 那麼必然有:a的完成時間晚於b(乙個scc的完成時間表示該scc中所有結點完成時間最晚的乙個)。

可以簡單證明一下:

如果我們在第一遍dfs的時候,a先於b被訪問,且a中的第乙個被訪問到的結點是x, 那a和b中所有的結點顯然都能在x之後被訪問到。於是x的完成時間要晚於b中任何乙個結點, 從而a的完成時間晚於b。

如果b先於a被訪問,由於a和b是不同的兩個scc,而且只有a到b的邊,於是b就不能到達a。 那麼當b中的結點被訪問完之後,a中的點仍然處於為訪問狀態,自然a的完成時間也就晚於b了。

所以在第二遍dfs時,第一次取到的那個完成時間最晚的結點u, 它所在的scc在轉置圖中就不能有指向外的邊。於是對轉置圖的第二遍dfs, 從u開始,便能輕易走遍所有處於同一scc的結點。後續的遍歷步驟也就類似了。

時間複雜度自然是算在兩次dfs頭上,o(v+e)。

tarjan貌似跟hopcroft都是cornell的大神。總的來說, tarjan演算法基於乙個觀察,即:同處於乙個scc中的結點必然構成dfs樹的一棵子樹。 我們要找scc,就得找到它在dfs樹上的根。

那麼怎麼找呢?

考慮一下,如果dfs訪問到了某個結點u,又順著u來到了結點v, 但從v發出了一條反向邊,指向了u的前驅w,那根據dfs的性質, u->v->w->u構成了乙個環。這一堆東西必然處於同乙個scc。 所以某個要找到scc子樹的根,就得找那個在dfs樹中最早被發現的結點,且這個結點要與它的一堆後繼結點形成環。

這時候dfs的特性就派上用場了。最早發現的結點可以通過記錄發現時間來實現,而反向邊的判斷可以通過結點顏色,即訪問狀態來實現。 定義乙個結點的low值為:從該節點的子樹結點可達的,尚未求出屬於哪個scc的結點的最早訪問時間。 由於scc構成子樹,所以求沒求出某個結點所在的scc用棧來刻畫就可以了: 每次訪問到乙個結點u,記錄發現時間visit,並將它推到棧裡去。 如果從u可達的結點v沒訪問過,那麼訪問v,用v的low值更新u; 否則,如果v已訪問過,那就看看它在不在棧中。如果在,說明還沒確定v到底屬於哪個scc, 這時(u, v)就是一條反向邊了,根據v的visit值,更新u的low值即可。 最後回到u結點時,如果u的low值和visit相等了,顯然u就是我們要找的根節點了。 從棧裡把u和其上所有結點彈出來,這一堆東西就在乙個scc裡了。上偽**:

1

2 34 5

6 78 9

1011

1213

1415

1617

1819

2021

2223

2425

2627

2829

30

// n: number of nodes in the graph

// visit[i]: discovery time of node i

// low[i]: low-value of node i

// time: the time stamp

// s: the stack

time <- 1

find_scc(g):

for i<-1 to n:

if visit[i] is not defined:

tarjan(i)

tarjan(u):

visit[u] <- time

low[u] <- time

time <- time + 1

push(s, u)

for each edge (u, i) in the graph:

if visit[i] is not defined:

tarjan(i)

low[u] <- min

else if i is in s:

low[u] <- min

// things popped here are in the same scc

if visit[u] == low[u]:

pop all node above u on stack including u

每個結點入棧一次出棧一次,每條邊訪問一次,o(v+e)。

gabow與tarjan的思路是一致的。但gabow使用了另乙個棧來找出scc子樹的根。 gabow使用的棧s與tarjan一樣,儲存尚未決定屬於哪個scc的結點; 棧p保持如下性質:棧頂結點始終具有最小的visit值, 即保持棧頂元素的visit值小於等於當前發現的反向邊指向的祖先結點的visit值。

棧s和p都隨著dfs的進行增長。若當前正在訪問結點u,從u可達點v, 先將u壓入兩個棧中。這一步驟相當於tarjan中初始化乙個結點的low值為當前visit值。 如果v沒有訪問過,則訪問v;否則判斷v是否在s棧中。 如果在,那麼(u, v)為反向邊,此時從p棧頂彈出那些晚於v被發現的結點。為啥? 因為此時v是u的後繼結點,我們得找出以u為根的子樹結點能到達的最早訪問的結點, 類似於tarjan演算法中對low值的更新。

再一次回到u時,若p棧棧頂的元素就是u,表明u就是scc子樹的根。 與tarjan類似,從s棧中彈出元素即找到了乙個scc,**:

1

2 34 5

6 78 9

1011

1213

1415

16

gabow(u):

visit[u] <- time

time <- time + 1

push(s, u)

push(p, u)

for each edge (u, i) in the graph:

if visit[i] is not defined:

gabow(i)

else if i is in s:

repeat popping nodes on p until visit[top(p)] <= visit[i]

// things popped here are in the same scc

if top(p) == u:

pop all node above u on s including u

pop u from p

gabow時間複雜度也為o(v+e),常數因子的差別各位大神請自行分析。

強連通分量 tarjan求強連通分量

雙dfs方法就是正dfs掃一遍,然後將邊反向dfs掃一遍。挑戰程式設計 上有說明。雙dfs 1 include 2 include 3 include 4 include 5 6using namespace std 7const int maxn 1e4 5 8 vector g maxn 圖的鄰...

強連通分量

對於有向圖的乙個頂點集,如果從這個頂點集的任何一點出發都可以到達該頂點集的其餘各個頂點,那麼該頂點集稱為該有向圖的乙個強連通分量。有向連通圖的全部頂點組成乙個強連通分量。我們可以利用tarjan演算法求強連通分量。define n 1000 struct edge e 100000 int ec,p...

強連通分量

在有向圖g中,如果兩個頂點間至少存在一條路徑,稱兩個頂點強連通 strongly connected 如果有向圖g的每兩個頂點都強連通,稱g是乙個強連通圖。非強連通圖有向圖的極大強連通子圖,稱為強連通分量 strongly connected components 下圖中,子圖為乙個強連通分量,因為...