圖的深度優先搜尋演算法並生成DFS樹
前面一篇文章介紹了圖的廣度優先搜尋演算法和BFS樹,這篇檔案筆者將介紹另一種圖的遍歷演算法-深度優先演算法
概述
深度優先搜尋(Depth-First Search,DFS)選取下一頂點的策略,可概括為:優先選取最後一個被訪問到的頂點的鄰居。以頂點 s 為基點的 DFS 搜尋,將首先訪問頂點 s;再從 s 所有尚未訪問到的鄰居中任取其一,並以之為基點,遞迴地執行 DFS 搜尋。故各頂點被訪問到的次序,類似於樹的先序遍歷而各頂點被訪問完畢的次序,則類似於樹的後序遍歷
圖的深度優先搜尋的程式碼實現
首先來看看再遍歷演算法中節點和弧使用到的屬性:
節點
private int status = 0 ; //狀態 0 undiscovered "未發現" 1 discovered "已發現" 2 visited "已完成"
private int parent = -1;
private int dTime = -1; //開始遍歷的時間
private int fTime = -1; //結束遍歷的時間
弧
private int type;
//弧型別:0 CROSS 跨邊 1 TREE(支撐樹)
//2 BACKWARD(該弧的起點和終點在支撐樹中存在終點到起點的路徑)
//3 FORWARD (該弧的起點和終點在支撐樹中存在其他路徑依然可以從起點到終點)
遍歷演算法
//從節點index開始遍歷
public void dfsTree(int index) {
this.reload(); //復位所有節點和弧狀態
//用來記錄某個節點被遍歷的時間
Integer clock = new Integer(0);
int s = index;
do {
if(allNodes[s].getStatus() == 0){
dfs(s, clock);
}
}while(index !=(s = (++s%size)));//按序號檢查,防止遺漏index無法連通的節點
}
public void dfs(int index, Integer clock) {
//發現該節點
allNodes[index].setStatus(1);
//記錄開始訪問的時間
allNodes[index].setdTime(++clock);
//找出節點index的所有鄰居
for(int i=0; i<size; i++) {
Edge edge = getEdge(index, i);
if(getEdge(index, i)!=null) {
switch(allNodes[i].getStatus()) {
//如果節點i尚未被發現
case 0:
//並設定支撐樹(index為i的parent)
allNodes[i].setParent(index);
//發現該節點
edge.setType(1);
//開始遞迴
dfs(i, clock);
break;
//如果節點i已被訪問但沒有完全被訪問,則i為i->parent->parent... == index
case 1:
edge.setType(2);
break;
//如果該節點已經被訪問完畢。則根據其遍歷結束時間來區分
case 2:
int type = allNodes[index].getdTime() > allNodes[i].getdTime() ? 0: 3;
edge.setType(type);
break;
}
}
}
//index節點訪問完畢
allNodes[index].setStatus(2);
allNodes[index].setfTime(++clock);
}
演算法的實質功能,由子演算法 dfs()遞迴地完成。每一遞迴例項中,都先將當前節點 v 標記為 “已發現” 狀態,再逐一核對其各鄰居 u 的狀態並做相應處理。待其所有鄰居均已處理完畢之後,將頂點 v 置為 “訪問完畢” 狀態,便可遞歸回溯。
若項點 u 尚處於 “未發現” 狀態,則將邊(v, u)歸類為樹邊,並將v置為u的parent。此後,便可將u作為當前頂點,繼續遞迴地遍歷。
若項點 u 處於 “已發現 ” 狀態,則意味著在此處發現一個有向環路。此時,在 DFS 遍歷樹中 u 必為 v 的祖先,故應將邊(v, u)歸類為後向邊BACKWARD。
這裡為每個頂點 v 都記錄了被發現的和訪問完成的時刻,對應的時間區間【dTime (v), fTime (v)】均稱作 v 的活躍期(active duration)。實際上,任意頂點 v 和 u 之間是否存在祖先 /後代的“血緣”關係,完全取決於二者的活躍期是否相互包含。
對於有向圖,頂點 u 還可能處於 “已發現” 狀態。此時,只要比對 v 與 u 的活躍期,即可判定在DFS 樹中 v 是否為 u 的祖先。若是,則邊(v, u)應歸類為前向邊FORWARD(forward edge);否則,二者必然來自相互獨立的兩個分支,邊(v, u)應歸類為跨邊(cross edge)。如果v的dTime()小則為前向邊,否則為跨邊
bfs (s)返回後,所有訪問過的頂點通過 parent指標依次聯接,從整體上給出了頂點 s 所屬連通或可達分量的一棵遍歷樹,稱作深度優先搜尋樹或 DFS 樹(DFS tree)。與 BFS 搜尋一樣,此時若還有其它的連通或可達分量,則可以其中任何頂點為基點,再次啟動 DFS 搜尋,來構成了 DFS 森林(DFS forest)。
例項
下圖針對含 7 個頂點和 18 條邊的某有向圖,給出了 DFS 搜尋的詳細過程。注意觀察頂點時間標籤的設定,頂點狀態的演變,邊的分類和結果,以及 DFS 樹(森林)的生長過程:
粗邊框白色,為當前頂點;細邊框白色、雙邊框白色和黑色,分別為處於未發現、已發現和已完成狀態的頂點;dTime和fTime標籤,分別標註與各頂點的左右
最終結果如圖t所示,為包含兩棵DFS樹的一個DFS森林。可以看出,選用不同的起始基點,生成的DFS樹(森林)也可能各異。如本例中,若從D開始搜尋,則DFS森林可能如圖u所示。
複雜度
除了原圖本身,深度優先搜尋演算法所使用的空間,主要消耗於各頂點的時間標籤和狀態標記,以及各邊的分類標記,二者累計不超過 0 (n) + O (e) = O (n + e)。當然,如採用以上程式碼的直接遞迴實現方式,作業系統為維護執行棧還需耗費一定量的空間一儘管這部分增量在漸進意義下還不足以動搖以上結論。
時間方面,不計對子函式 dfs()的呼叫,dfsTree()本身對所有頂點的列舉共需 0 (n)時間。不計 dfs()之間相互的遞迴呼叫,每個頂點、每條邊只在子函式 dfs()的某一遞迴例項中耗費 O (1)時間,故累計亦不過 0 (n + e)時間。綜合而言,深度優先搜尋演算法也可在 O (n + e)時間內完成。