強連通分支算法
本節內容將詳細討論有向圖的強連通分支算法(strongly connected component),該算法是圖深度優先搜索算法的另一重要應用。強分支算法可以將一個大圖分解成多個連通分支,某些有向圖算法可以分別在各個聯通分支上獨立運行,最后再根據分支之間的關系將所有的解組合起來。
在無向圖中,如果頂點s到t有一條路徑,則可以知道從t到s也有一條路徑;在有向無環圖中個,如果頂點s到t有一條有向路徑,則可以知道從t到s必定沒有一條有向路徑;對於一般有向圖,如果頂點s到t有一條有向路徑,但是無法確定從t到s是否有一條有向路徑。可以借助強連通分支來研究一般有向圖中頂點之間的互達性。
有向圖G=(V, E)的一個強連通分支就是一個最大的頂點子集C,對於C中每對頂點(s, t),有s和t是強連通的,並且t和 s也是強連通的,即頂點s和t是互達的。圖中給出了強連通分支的例子。我們將分別討論3種有向圖中尋找強連通分支的算法。
3種算法分別為Kosaraju算法、Tarjan算法和Gabow算法,它們都可以在線性時間內找到圖的強連通分支。
Kosaraju算法
Kosaraju算法的解釋和實現都比較簡單,為了找到強連通分支,首先對圖G運行DFS,計算出各頂點完成搜索的時間f;然后計算圖的逆圖GT,對逆圖也進行DFS搜索,但是這里搜索時頂點的訪問次序不是按照頂點標號的大小,而是按照各頂點f值由大到小的順序;逆圖DFS所得到的森林即對應連通區域。具體流程如圖(1~4)。
上面我們提及原圖G的逆圖GT,其定義為GT=(V, ET),ET={(u, v):(v, u)∈E}}。也就是說GT是由G中的邊反向所組成的,通常也稱之為圖G的轉置。在這里值得一提的是,逆圖GT和原圖G有着完全相同的連通分支,也就說,如果頂點s和t在G中是互達的,當且僅當s和t在GT中也是互達的。

根據上面對Kosaraju算法的討論,其實現如下:
* 算法1:Kosaraju,算法步驟:
* 1. 對原始圖G進行DFS,獲得各節點的遍歷次序ord[];
* 2. 創建原始圖的反向圖GT;
* 3. 按照ord[]相反的順序訪問GT中每個節點;
* @param G 原圖
* @return 函數最終返回一個二維單鏈表slk,單鏈表
* 每個節點又是一個單鏈表, 每個節點處的單鏈表表示
* 一個聯通區域;slk的長度代表了圖中聯通區域的個數。
*/
public static SingleLink2 Kosaraju(GraphLnk G){
SingleLink2 slk = new SingleLink2();
int ord[] = new int[G.get_nv()];
// 對原圖進行深度優先搜索
GraphSearch.DFS(G);
// 拷貝圖G的深度優先遍歷時每個節點的離開時間
for( int i = 0; i < GraphSearch.f.length; i++){
ord[i] = GraphSearch.f[i];
System.out.print(GraphSearch.parent[i] + " || ");
}
System.out.println();
// 構造G的反向圖GT
GraphLnk GT = Utilities.reverseGraph(G);
/* 用針對Kosaraju算法而設計DFS算法KosarajuDFS函數
* 該函數按照ord的逆向順序訪問每個節點,
* 並向slk中添加新的鏈表元素; */
GraphSearch.KosarajuDFS(GT, ord, slk);
// 打印所有的聯通區域
for(slk.goFirst(); slk.getCurrVal()!= null; slk.next()){
// 獲取一個鏈表元素項,即一個聯通區域
GNodeSingleLink comp_i =
(GNodeSingleLink)(slk.getCurrVal().elem);
// 打印這個聯通區域的每個圖節點
for(comp_i.goFirst();
comp_i.getCurrVal() != null; comp_i.next()){
System.out.print(comp_i.getCurrVal().elem + "\t");
}
System.out.println();
}
// 返回聯通區域鏈表
return slk;
}
算法首先對原圖進行DFS搜索,並記錄每個頂點的搜索離開時間ord;然后調用Utilities類中的求逆圖函數reverseGraph,求得原圖的逆圖GT;最后調用GraphSearch類的遞歸函數KosarajuDFS,該函數按照各頂點的ord時間對圖進行深度優先搜索。函數最終返回一個二維單鏈表slk,單鏈表每個節點的元素項也是一個單鏈表,每個節點處的單鏈表表示一個聯通區域,如圖,slk的長度代表了圖中聯通區域的個數。之所以使用這樣的數據結構作為函數的結果,其原因是在函數返回之前無法知道圖中共有多少個強連通分支,也不知道每個分支的頂點的個數,二維鏈表自然成為最優的選擇。其余兩個算法的返回結果也采用這種形式的鏈表輸出結果。

圖 Kosaraju算法返回的二維鏈表形式結果
reverseGraph函數返回的逆圖是新創建的圖,其每條邊都與原圖邊的方向相反。原圖中一個頂點對應的鏈表中的相鄰頂點是按照標號由小到大的順序排列的,這樣做能提高相關算法的效率(例如isEdge(int, int)函數)。這里在計算逆圖時這個條件也必須滿足。該靜態函數的實現如下:
* 創建一個與入參G每條邊方向相反的圖,即逆圖。
* @param G 原始圖
* @return 逆圖
*/
public static GraphLnk reverseGraph(GraphLnk G){
GraphLnk GT = new GraphLnk(G.get_nv());
for( int i = 0; i < G.get_nv(); i++){
// GT每條邊的方向與G的方向相反
for(Edge w = G.firstEdge(i); G.isEdge(w);
w = G.nextEdge(w)) {
GT.setEdgeWt(w.get_v2(), w.get_v1(),
G.getEdgeWt(w));
}
}
return GT;
}
函數中對頂點按照標號由小到大進行遍歷,對頂點i,再遍歷其對應鏈表中的頂點i0, …, in,並向逆圖中添加邊(i0, i), …, (in, i),最終得到原圖的逆圖GT。可以發現,函數中調用setEdgeWt函數來向GT中添加邊。細心的讀者可能還記得,若待修改權值的邊不存在時,函數setEdgeWt充當添加邊的功能,並且能保證相鄰的所有頂點的標號在鏈表中按由小到大的順序排列。
Kosaraju實現中關鍵的一步是調用KosarajuDFS函數對逆圖GT進行遞歸深度優先搜索。函數的另外兩個形參為原圖DFS所得的各頂點搜索結束時間ord,和保存連通分支結果的鏈表slk。KosarajuDFS的實現如下:
* 將根據逆圖GT和每個節點在原圖G深度遍歷時的離開時間數組,
* 生成鏈表slk表示的連通分支
* 本函數不改變圖的連接關系,只是節點的訪問次序有所調整.
* @param GT 逆圖
* @param ord 原圖DFS各頂點遍歷離開時間數組
* @param slk 連通分支存放在鏈表中
*/
public static void KosarajuDFS(GraphLnk GT, int ord[],
SingleLink2 slk){
/* 根據ord數組計算new_order數組,新的訪問順序為:
* 第i次訪問的節點為原圖上的第new_order[i]個節點 */
int new_order[] = new int[ord.length];
// 調用函數newordermap改變數組的次序
newordermap(ord, new_order);
int n = GT.get_nv();
// 這里只需要記錄顏色,其它信息不重要了
color = new COLOR[n];
// 顏色初始化為白色
for( int i = 0; i < n; i++)
color[i] = COLOR.WHITE;
// 為找到圖中所有的聯通區域,循環迭代:
for( int i = 0; i < new_order.length; i++){
// 第i次訪問的節點為原圖上的第new_order[i]個節點
int j= new_order[i];
if(color[j] != COLOR.WHITE)
continue;
// 創建一個圖節點鏈表,表示一個聯通區域
GNodeSingleLink gnsk = new GNodeSingleLink();
// 調用遞歸函數,以j為起點深度搜索該圖
KosarajuDFSVISIT(GT, ord, j, gnsk);
/* 將聯通區的節點形成的鏈表添加到聯通區域鏈表中,
* 這里使用的實際上是二維鏈表 */
slk.append( new ElemItem<GNodeSingleLink>(gnsk));
}
}
函數首先根據ord數組確定GT中各頂點的訪問次序,方法是創建大小與ord相等的數組new_ord,並調用newordermap函數給new_ord各元素賦值。new_ord[i]的意義是:原圖上第i個節點在逆圖的訪問次序為new_ord[i]。
例如頂點在原圖中DFS遍歷離開時間ord為:
11 |
0)16 |
1)10 |
2)15 |
3)9 |
4)14 |
5)8 |
6)13 |
7)7 |
8)20 |
9)19 |
則各頂點在逆圖中訪問先后次序為:
8, 9, 0, 2, 4, 6, 1, 3, 5, 7,
newordermap函數實現如下:
* 根據原圖各頂點DFS得到的遍歷結束時間,獲取節點新
* 的訪問次序.在函數返回時,new_ord[i]的意義是:
* 原圖上第i個節點在逆圖的訪問次序為new_ord[i];
* 其效果是:后訪問的節點在新的訪問次序中先訪問.
* @param ord 原圖各頂點DFS遍歷結束時間
* @param new_ord 節點在逆圖的訪問次序
*/
public static void newordermap( int[] ord, int[] new_ord){
// 為防止原ord數組被破壞,對其深拷貝
int ord_temp[] = new int[ord.length];
for( int i = 0; i < ord.length; i++)
ord_temp[i] = ord[i];
int max, max_idx;
// 在ord_temp中尋找n次最大的值,並將其數組下標放
// 置到new_ord中;然后將最大值賦值為-1
for( int i = 0; i < ord.length; i++){
max_idx = 0;
max = ord_temp[max_idx];
for( int j = 0; j < ord.length; j++){
if(ord_temp[j] == -1)
continue;
if(ord_temp[j] > max){
max = ord_temp[j];
max_idx = j;
}
}
new_ord[i] = max_idx;
ord_temp[max_idx] = -1;
}
}
確定各頂點的訪問次序后,按照new_ord中頂點的順序調用遞歸函數KosarajuDFSVISIT對逆圖進行深度優先搜索。每次調用前都創建一個新的鏈表作為slk鏈表的節點,對應一個新的強連通分支。
* 遞歸函數,起點為u深度搜索圖G,節點遞歸地調用該函數時,節點的訪問順序
* 由ord決定,對節點u與之相連且標記為白色的的節點v1,v2,...
* 先訪問ord[vi]最大的節點vi.當沒有與節點u相連的節點或者與u相連的所有
* 節點都不是白色的了,此時獲得一個聯通區域,函數返回
*/
public static void KosarajuDFSVISIT(GraphLnk G, int ord[],
int u, GNodeSingleLink slk){
// 訪問該節點,將其顏色標記為灰色
color[u] = COLOR.GRAY;
// 將該節點u添加到當前的聯通區域中
slk.append( new ElemItem<Integer>(u));
// 首先統計與該節點相連的、顏色為白色的節點的個數
int cnt = 0;
for(Edge w = G.firstEdge(u);
G.isEdge(w); w = G.nextEdge(w)){
if(color[w.get_v2()] == COLOR.WHITE)
cnt++;
}
// 如果此時沒有與該節點相連並且顏色為白色的節點,函數返回
if(cnt == 0)
return;
// 否則,將與該節點相連的、白色節點暫存至數組e中
Edge e[] = new Edge[cnt];
cnt = 0;
for(Edge w = G.firstEdge(u); G.isEdge(w); w = G.nextEdge(w)){
if(color[w.get_v2()] == COLOR.WHITE)
e[cnt++] = w;
}
/* 對數組e按照邊的終點的訪問次序ord[..]進行排序
* 這里采用選擇排序來完成這一過程 */
int max_idx;
for( int i = 0; i < e.length - 1; i++){
// 第i輪找第i大的元素
max_idx = i;
for( int j = i + 1; j < e.length; j++){
if(ord[e[j].get_v2()] > ord[e[max_idx].get_v2()]){
max_idx = j;
}
}
// 如果原先第i位置上不是最大的,則交換操作
if(max_idx != i){
Edge t = e[i];
e[i] = e[max_idx];
e[max_idx] = t;
}
}
// 對排序后的邊逐個進行遞歸調用
for( int i = 0; i < e.length; i++)
KosarajuDFSVISIT(G, ord, e[i].get_v2(), slk);
}
函數中遞歸搜索頂點u的相鄰頂點時,首相將u的所有尚未訪問的相鄰頂點(邊)保存至一個邊數組e中。然后再對這個數組中的邊進行重排序,排序的規則依然是在原圖中頂點DFS搜索離開時間。圖(a)的運行結果如圖所示。
這里不對Kosaraju算法進行詳細證明,感興趣的讀者可以參閱相關的圖算法書籍。
Tarjan算法
Kosaraju算法的流程簡單,但是需要對圖(和逆圖)進行兩次DFS搜索,而且讀逆圖的DFS搜索中頂點的訪問順序有特定的限制。下面將介紹的兩個算法的過程比Kosaraju算法更為簡潔,只需要執行一次DFS,並且不需要計算逆圖。
Tarjan基於遞歸實現的深度優先搜索,在搜索過程中將頂點不斷壓入堆棧中,並在回溯時判斷堆棧中頂點是否在同一聯通分支。函數借助兩個輔助數組pre和low,其中pre[u]為頂點u搜索的次序編號,low[u]為頂點u能回溯到的最早的頂點的次序編號。當pre[u]=low[u]時,則彈出棧中頂點並構成一個連通分支。以一個簡單的例子來解釋這一過程,如圖所示,

遞歸 |
回溯 |
||||||
棧 |
0 |
2 |
1 |
||||
頂點 |
0 |
2 |
1 |
0 |
1 |
2 |
0 |
pre |
0 |
1 |
2 |
2 |
1 |
0 |
|
low |
0 |
1 |
2 |
0 |
0 |
0 |
尋找圖中連通分支的過程
對圖中的簡單聯通圖,首先遞歸地對圖進行深度優先搜索,並記錄每個頂點的搜索次序pre。搜索起點為0,當對頂點1進行遞歸時將再次達到頂點0;在回溯過程中依次將頂點1和頂點2的low值修改為low[0]。當回溯到頂點0時將棧中low值為low[0]的頂點彈出並組成一個連通分支。
Tarjan算法實現如下:
* Trajan 算法實現圖的強連通區域求解;
* 算法步驟:
* @param G 待求解的圖
* @return
* 一個二維單鏈表slk,單鏈表每個節點也是一個單鏈表,
* 每個節點處的單鏈表表示一個聯通區域;
* slk的長度代表了圖中聯通區域的個數。
*/
public static SingleLink2 Tarjan(GraphLnk G){
SingleLink2 slk = new SingleLink2();
// 算法需借助於棧結構
LinkStack ls = new LinkStack();
int pre[] = new int[G.get_nv()];
int low[] = new int[G.get_nv()];
int cnt[] = new int[1];
// 初始化
for( int i = 0; i < G.get_nv(); i++){
pre[i] = -1;
low[i] = -1;
}
// 對頂點運行遞歸函數TrajanDFS
for( int i = 0; i < G.get_nv(); i++){
if(pre[i] == -1) {
GraphSearch.TarjanDFS(G,
i, pre, low, cnt,
ls, slk);
}
}
// 打印所有的聯通區域
for(slk.goFirst(); slk.getCurrVal() != null; slk.next()){
// 獲取一個鏈表元素項,即一個聯通區域
GNodeSingleLink comp_i =
(GNodeSingleLink)(slk.getCurrVal().elem);
// 打印這個聯通區域的每個圖節點
for(comp_i.goFirst();
comp_i.getCurrVal() != null; comp_i.next()){
System.out.print(comp_i.getCurrVal().elem + "\t");
}
System.out.println();
}
return slk;
}
函數首先申明數組變量pre, low和cnt,並對初始化。pre 和low的意義前面已經解釋過,cnt是長度為1的數組。因為在函數TarjanDFS中cnt的值需要不斷變化,如果直接用變量作為形參,函數並不會改變cnt的值,所以需要使用數組。除了數組變量之外,還有一個棧ls,其作用是在對圖進行深度優先搜索時記錄遍歷頂點,並在回溯時彈出部分頂點形成連通分支。
函數關鍵的步驟是對圖中各頂點調用遞歸函數TarjanDFS,該函數以深度優先搜索算法為基礎,在遞歸和回溯時不斷對pre, low以及棧ls進行操作。實現如下:
* Tarjan算法的遞歸DFS函數
*/
public static void TarjanDFS(GraphLnk G, int w,
int pre[], int low[], int cnt[],
LinkStack ls, SingleLink2 slk){
int t , min = low[w] = pre[w] = cnt[0]++;
// 將當前頂點號壓棧
ls.push( new ElemItem<Integer>(w));
// 對當前頂點的每個鄰點循環
for(Edge e = G.firstEdge(w);
G.isEdge(e);
e = G.nextEdge(e)){
// 如果鄰點沒有遍歷過,則對其遞歸調用
if(pre[e.get_v2()] == -1){
TarjanDFS(G, e.get_v2(),
pre, low, cnt,
ls, slk);
}
/* 如果鄰點已經被遍歷過了,比較其編號與min,
* 如果編號較小,則更新min的大小 */
if(low[e.get_v2()] < min)
min = low[e.get_v2()];
}
// 如果此時min小於low[w],則返回
if(min < low[w]){
low[w] = min;
return;
}
// 否則,彈出棧中元素,並將元素添加到鏈表中
GNodeSingleLink gnslk = new GNodeSingleLink();
do{
// 彈出棧頂元素
t = ((Integer)(ls.pop().elem)).intValue();
low[t] = G.get_nv();
// 添加到鏈表中
gnslk.append( new ElemItem<Integer>(t));
} while(t != w);
// 將該鏈表添加到slk鏈表中
slk.append( new ElemItem<GNodeSingleLink>(gnslk));
}
下面以圖中有向圖為例,分析Tarjan算法求解較為復雜的有向圖的過程。各頂點的搜索訪問次序以及low值的變化如圖。
頂點 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
次序 |
0 |
2 |
1 |
3 |
6 |
4 |
7 |
5 |
8 |
9 |
low |
0 |
2/0 |
1/0 |
3 |
6 |
4/3 |
7/6 |
5 |
8 |
9/8 |
min |
0 |
2/0 |
1/0 |
3 |
6 |
4/3 |
7/6 |
5 |
8 |
9/8 |
深度優先搜索各頂點的訪問次序以及堆棧中頂點的變化過程如圖。其中框出來的步驟表示遍歷到達已訪問過的頂點,此時需要對low做修改,堆棧中黑體加粗的頂點表示彈出的頂點。

圖 Tarjan算法詳細過程圖
算法詳細過程分析如下:
搜索起點為頂點0,遞歸搜索頂點2、頂點1,這三個頂點被壓入棧中。在頂點1處首先遞歸搜索頂點0,由於頂點0已經訪問過,所以不再遞歸搜索頂點0,而是作如下處理:
如果low[0] < 頂點1的min(記為min1),則min1 ← low[0],
此時(0=low[0])<(min1=1),所以min1←0。
接着到達頂點3,頂點3尚未被訪問,則遞歸搜索頂點3,並繼續搜索頂點5。此時,頂點5第一個相鄰點為頂點3,則不再遞歸搜索頂點3,更新min5值,min5←3。
接着遞歸搜索頂點7,頂點7只有一個相鄰頂點,即為頂點自身,則頂點7的遞歸結束。回溯到頂點7,此時min7=low[7]=5,彈出棧中頂點7,並作為第一個聯通分支。
函數繼續回溯到頂點5,此時頂點5的相鄰頂點的遞歸搜索已經全部結束,由於(3=min5)<(low[5]=5),則low[5]←(min5=3),並返回;函數回溯到頂點3,此時與頂點3相鄰的頂點的遞歸搜索也已全部完成,由於min3=low[3]=3,所以彈出棧中頂點5和頂點3,作為第二個連通分支。
函數回溯至頂點1,此時與頂點1相鄰的頂點的遞歸調用已結束,此時(0=min1)<(low[1]=2),則low[1]←(min1=0),並返回;函數回溯頂點2,更新min2←(low[1]=0);此時頂點2還有兩個相鄰頂點,即頂點3和頂點4,頂點3已經判定屬於第二個連通分支,所以對頂點4遞歸搜索。
首先將頂點4壓入棧中,對相鄰頂點5遞歸搜索,頂點5已經判定屬於第二個連通分支,所以轉而對頂點6遞歸搜索。頂點6的第一個相鄰頂點為4,則不再遞歸搜索頂點4,更新頂點6的min6←(low[4]=6)。頂點6的第二個相鄰頂點為7,但頂點7已經判定屬於第一個連通分支。此時頂點6的所有相鄰頂點遞歸搜索結束,此時(6=min6)<(low[6]=7),則low[6]←(min6=6),並返回;函數回溯到頂點4,與頂點4相鄰的頂點的遞歸搜索也已全部完成,由於min4=low[4]=6,所以彈出棧中頂點6和頂點4,作為第三個連通分支。
函數回溯至頂點2,此時頂點2的所有相鄰頂點遞歸搜索結束,此時(0=min2)<(low[2]=1),則low[2]←(min2=0),並返回;
函數回溯至頂點0,此時頂點0的所有相鄰頂點遞歸搜索結束,並且頂點min0=low[0],則將棧中頂點1,2,0依次彈出,作為第四個連通分支。
最后函數繼續遞歸搜索頂點8和頂點9(略)。
從算法的流程可以清楚的發現Tarjan算法可以在線性時間內找出一個有向圖的強分量,並且對原圖進行一次DFS即可。
.Gabow算法
Gabow算法是Tarjan算法的提升版本,該算法類似於Tarjan算法。算法維護了一個頂點棧,但是還是用了第二個棧來確定何時從第一個棧中彈出各個強分支中的頂點,它的功能類似於Tarjan算法中的數組low。從起始頂點w處開始進行DFS過程中,當一條回路顯示這組頂點都屬於同一個強連通分支時,就會彈出棧二中頂點,只留下回邊的目的頂點,也即搜索的起點w。
當回溯到遞歸起始頂點w時,如果此時該頂點在棧二頂部,則說明該頂點是一個強聯通分量的起始頂點,那么在該頂點之后搜索的頂點都屬於同一個強連通分支。於是,從第一個棧中彈出這些點,形成一個強連通分支。
根據上面對算法的描述,實現如下:
* @param G 輸入為圖結構
* @return:
* 函數最終返回一個二維單鏈表slk,單鏈表每個節點又是一個單鏈表,
* 每個節點處的單鏈表表示一個聯通區域;
* slk的長度代表了圖中聯通區域的個數。
*/
public static SingleLink2 Gabow(GraphLnk G){
SingleLink2 slk = new SingleLink2();
// 函數使用兩個堆棧
LinkStack ls = new LinkStack();
LinkStack P = new LinkStack();
int pre[] = new int[G.get_nv()];
int cnt[] = new int[1];
// 標注各個頂點所在的連通分支的名稱
int id[] = new int[G.get_nv()];
// 初始化
for( int i = 0; i < G.get_nv(); i++){
pre[i] = -1;
id[i] = -1;
}
for( int i = 0; i < G.get_nv(); i++){
if(pre[i] == -1) {
GraphSearch.GabowDFS(G,
i, pre, id, cnt,
ls, P, slk);
}
}
// 打印所有的聯通區域
for(slk.goFirst(); slk.getCurrVal() != null; slk.next()){
// 獲取一個鏈表元素項,即一個聯通區域
GNodeSingleLink comp_i =
(GNodeSingleLink)(slk.getCurrVal().elem);
// 打印這個聯通區域的每個圖節點
for(comp_i.goFirst();
comp_i.getCurrVal() != null; comp_i.next()){
System.out.print(comp_i.getCurrVal().elem + "\t");
}
System.out.println();
}
return slk;
}
函數調用遞歸實現的深度優先搜索GabowDFS,實現如下:
/**
* GabowDFS算法的遞歸DFS函數
* @param ls 棧1,
* @param P 棧2,決定何時彈出棧1中頂點
*/
public static void GabowDFS(GraphLnk G, int w,
int pre[], int[] id, int cnt[],
LinkStack ls, LinkStack P,
SingleLink2 slk){
int v;
pre[w] = cnt[0]++;
// 將當前頂點號壓棧
ls.push( new ElemItem<Integer>(w));
System.out.print("+0 stack1 ");ls.printStack();
P.push( new ElemItem<Integer>(w));
System.out.print("+0 stack2 ");P.printStack();
// 對當前頂點的每個鄰點循環
for(Edge e = G.firstEdge(w); G.isEdge(e); e = G.nextEdge(e)){
// 如果鄰點沒有遍歷過,則對其遞歸調用
if(pre[e.get_v2()] == -1){
GabowDFS(G, e.get_v2(), pre, id, cnt, ls, P, slk);
}
// 否則,如果鄰點被遍歷過但又沒有被之前的連通域包含,則循環彈出
else if(id[e.get_v2()] == -1){
int ptop = ((Integer)(P.getTop().elem)).intValue();
// 循環彈出,直到棧頂頂點的序號不小於鄰點的序號
while(pre[ptop] > pre[e.get_v2()]) {
P.pop();
System.out.print("-1 stack2 ");P.printStack();
ptop = ((Integer)(P.getTop().elem)).intValue();
}
}
}
// 遍歷完頂點的所有相鄰頂點后,如果棧2頂部頂點與w相同則彈出;
if(P.getTop() != null
&& ((Integer)(P.getTop().elem)).intValue() == w){
P.pop();
System.out.print("-2 stack2 ");P.printStack();
}
// 否則函數返回
else return;
// 如果棧2頂部頂點與w相同,則彈出棧1中頂點,形成連通分支
GNodeSingleLink gnslk = new GNodeSingleLink();
do{
v = ((Integer)(ls.pop().elem)).intValue();
id[v] = 1;
gnslk.append( new ElemItem<Integer>(v));
} while(v != w);
System.out.print("-3 stack1 ");ls.printStack();
slk.append( new ElemItem<GNodeSingleLink>(gnslk));
}
以圖中有向圖為例,並在深度優先搜索過程中跟蹤記錄兩個棧中元素的變化如下:
0.
+0 stack2 當前棧從棧頂至棧底元素為:
0.
+0 stack1 當前棧從棧頂至棧底元素為:
2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
2, 0.
+0 stack1 當前棧從棧頂至棧底元素為:
1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
1, 2, 0.
-1 stack2 當前棧從棧頂至棧底元素為:
2, 0.
-1 stack2 當前棧從棧頂至棧底元素為:
0.
+0 stack1 當前棧從棧頂至棧底元素為:
3, 1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
3, 0.
+0 stack1 當前棧從棧頂至棧底元素為:
5, 3, 1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
5, 3, 0.
-1 stack2 當前棧從棧頂至棧底元素為:
3, 0.
+0 stack1 當前棧從棧頂至棧底元素為:
7, 5, 3, 1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
7, 3, 0.
-2 stack2 當前棧從棧頂至棧底元素為:
3, 0.
-3 stack1 當前棧從棧頂至棧底元素為:
5, 3, 1, 2, 0.
-2 stack2 當前棧從棧頂至棧底元素為:
0.
-3 stack1 當前棧從棧頂至棧底元素為:
1, 2, 0.
+0 stack1 當前棧從棧頂至棧底元素為:
4, 1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
4, 0.
+0 stack1 當前棧從棧頂至棧底元素為:
6, 4, 1, 2, 0.
+0 stack2 當前棧從棧頂至棧底元素為:
6, 4, 0.
-1 stack2 當前棧從棧頂至棧底元素為:
4, 0.
-2 stack2 當前棧從棧頂至棧底元素為:
0.
-3 stack1 當前棧從棧頂至棧底元素為:
1, 2, 0.
-2 stack2 當前棧為空!
-3 stack1 當前棧為空!
+0 stack1 當前棧從棧頂至棧底元素為:
8.
+0 stack2 當前棧從棧頂至棧底元素為:
8.
+0 stack1 當前棧從棧頂至棧底元素為:
9, 8.
+0 stack2 當前棧從棧頂至棧底元素為:
9, 8.
-1 stack2 當前棧從棧頂至棧底元素為:
8.
-2 stack2 當前棧為空!
算法的流程這里不做詳細分析,讀者可以參見Tarjan算法的分析流程,並自行體會算法的思想。