圖的廣度優先搜索
圖的的搜索算法主要分為廣度優先搜索(breadth-first search或BFS)和深度優先搜索(depth-first search或DFS)。首先討論廣度優先搜索算法。
稱之為廣度優先,是因為算法始終首先發現距離起始頂點較近的頂點,然后才發現較遠的頂點。假設搜索的出發頂點為s,則首先搜索與s直接相鄰的頂點,然后再搜索這些相鄰頂點的相鄰頂點。在搜索過程中可以記錄每個頂點到起始頂點s的距離。這種搜索算法能生成一棵以s為根、包括所有s可達的頂點的廣度優先搜索樹(BFS樹)。圖中各頂點的訪問次序對應於廣度優先搜索樹中各節點由頂至底的層次。
在這里我們設計算法跟蹤圖中各個頂點的訪問次序,記錄各個頂點在BFS樹中的層數(搜索深度)以及父頂點。首先將各個頂點着白色,在跟蹤各個頂點訪問次序時,第一次被訪問的頂點的顏色改變為灰色,直至與之相鄰的所有頂點都被訪問時顏色改變為黑色。在這個過程中顏色為黑色和白色的頂點之間被顏色為灰色的頂點分割開來。廣度優先搜索算法的實現借助於隊列結構,代碼如下:
* 廣度優先搜索算法,從第start節點開始進行搜索;
* 算法中利用到隊列數據結構。
* @param G 待搜索的圖
* @param start 搜索的起點
*/
static void BFS(GraphLnk G, int start){
int n = G.get_nv();
if(start < 0 || start >= n) return;
color = new COLOR[n];
// 節點的父節點,非負整數
parent = new int[n];
// 深度depth
d = new int[n];
// 初始化
for( int i = 0; i < n; i++){
// 將所有頂點的顏色着色為白色
color[i] = COLOR.WHITE;
// 所有頂點的父母初始化為-1
parent[i] = -1;
// 所有頂點的深度初始化為無窮大
d[i] = Integer.MAX_VALUE;
}
// 起點着色為灰色
color[start] = COLOR.GRAY;
// 起點無父節點,記為-1
parent[start] = -1;
// 起點的深度為0
d[start] = 0;
// 創建隊列
LinkQueue Q = new LinkQueue();
// 將起點添加到隊列中
Q.enqueue( new ElemItem<Integer>(start));
// 迭代第對隊列首頂點的相鄰邊重新着色
while(Q.currSize() > 0){
// 隊列首頂點u
int u = ((Integer)(Q.dequeue().elem)).intValue();
for(Edge w = G.firstEdge(u);
G.isEdge(w);
w = G.nextEdge(w)){
/* 如果u的相鄰頂點第一次被發現了,則將其顏色改變為灰色,
* 其深度增加1,其父頂點為u,並將該頂點添加到隊列 */
if(color[w.get_v2()] == COLOR.WHITE){
color[w.get_v2()] = COLOR.GRAY;
d[w.get_v2()] = d[u] + 1;
parent[w.get_v2()] = u;
Q.enqueue( new ElemItem<Integer>(w.get_v2()));
}
}
// 搜索了頂點u的所有相鄰頂點后,將其顏色改變為黑色
color[u] = COLOR.BLACK;
// 打印此時各個頂點的顏色
for( int i = 0; i < n; i++){
System.out.print(i + ")" + color[i] + "\t");
}
System.out.println();
}
// 打印所有頂點的搜索深度
System.out.println("各頂點的深度為:");
for( int i = 0; i < n; i++){
System.out.print(d[i] + "\t");
}
// 打印所有頂點的父頂點
System.out.println("\n各頂點的父母為:");
for( int i = 0; i < n; i++){
System.out.print(parent[i] + "\t");
}
System.out.println();
}
從函數的實現可以發現,廣度優先搜索算法是迭代實現的。迭代過程借助隊列結構暫存着灰色的頂點。首先訪問頂點s,並將與其相鄰的、未訪問過的頂點都添加到隊列中;然后迭代地將隊列中頂點彈出、對其訪問,並將與其相鄰的、未訪問過的頂點添加到隊列中。在前面章節我們討論過,隊列中元素項遵循先入先出(FIFO)的規則;迭代直至隊列為空為止。在BFS算法中FIFO規則保證了圖中頂點的訪問次序:先訪問的頂點距離起始頂點更近。
依然以圖(a)為例來分析廣度優先算法的過程以及廣度優先樹的意義,搜索的起點為頂點1。測試示例代碼為:
GraphLnk GL =
Utilities.BulidGraphLnkFromFile("Graph\\graph1.txt");
GraphSearch.BFS(GL, 1);
運行結果為:
0)GRAY1)BLACK2)WHITE3)WHITE4)WHITE5)GRAY6)WHITE7)WHITE
0)BLACK1)BLACK2)WHITE3)WHITE4)GRAY5)GRAY6)WHITE7)WHITE
0)BLACK1)BLACK2)GRAY3)WHITE4)GRAY5)BLACK6)GRAY7)WHITE
0)BLACK1)BLACK2)GRAY3)WHITE4)BLACK5)BLACK6)GRAY7)WHITE
0)BLACK1)BLACK2)BLACK3)GRAY4)BLACK5)BLACK6)GRAY7)WHITE
0)BLACK1)BLACK2)BLACK3)GRAY4)BLACK5)BLACK6)BLACK7)GRAY
0)BLACK1)BLACK2)BLACK3)BLACK4)BLACK5)BLACK6)BLACK7)GRAY
0)BLACK1)BLACK2)BLACK3)BLACK4)BLACK5)BLACK6)BLACK7)BLACK
各頂點的深度為:
10232123
各頂點的父母為:
1-1520156
搜索過程中各頂點顏色及隊列中灰色頂點的變化過程如圖(1~8),每張圖對應隊列中元素變化時的狀態。

圖 廣度優先搜索過程
根據廣度優先搜索算法得出的個頂點到起始點的距離可以得出廣度優先搜索樹如下:

圖 BFS樹
廣度優先搜索算法可以得到從給定起始頂點到每個可達頂點的距離,並且這個距離是頂點到起始頂點的最短路徑,具體的證明過程請參閱相關圖論數據。
. 圖的深度優先搜索
圖的另一種搜索算法稱為深度優先搜索(DFS),正如算法的名稱那樣,DFS算法總是盡可能“深”地搜索圖。在DFS過程中,某個頂點v在被訪問后,將遞歸地訪問第一個尚未被訪問過的相鄰點,直至沒有相鄰點可訪問為止;然后再回溯到頂點v,繼續遞歸訪問v的第二個尚未訪問過的相鄰點。這一過程一直進行到已訪問了起點可達的所有頂點為止。
除了遞歸方式之外,還可以借助堆棧采用迭代方式實現。首先訪問起點v,然后把頂點v及其相連的邊壓入棧中;彈出並訪問棧頂元素,棧頂元素對應的邊的另一頂點就是下一個訪問的頂點,對其重復頂點v的操作;迭代過程直至棧中沒有元素為止。
無論采用遞歸方式還是采用迭代方式,其結果都是首先沿着圖的一條路徑搜索直至它的末端,然后回溯,並沿着另一路徑搜索。這里我們主要實現DFS算法的遞歸方式。
在圖的廣度優先搜索算法中我們記錄了圖中每個頂點的深度,並在搜索過程中對各頂點進行着色的變化。在深度優先搜索算法中,我們將同時記錄訪問頂點的兩個時間(次序),第一個是開始訪問頂點的時間d,另一個是訪問完該頂點的所有相鄰頂點的時間f。根據這兩個時間的定義,對於頂點v,在時間dv之前頂點v一直為白色,在時間dv到fv之間頂點v為灰色,fv之后頂點為黑色。
深度優先搜索算法實現如下:
* 深度優先搜索算法,從第start個節點開始搜索
* @param G 待搜索的圖
* @param start 搜索起點
*/
static void DFS(GraphLnk G, int start){
int n = G.get_nv();
// 時間戳更新計數器置0
time = 0;
if(start < 0 || start >= n)
return;
// 各頂點的着色
color = new COLOR[n];
// 節點的父節點,非負整數
parent = new int[n];
// 第一個時間戳
d = new int[n];
// 第二個時間戳
f = new int[n];
// 每個節點的訪問次序
ord = new int[n];
// 初始化
for( int i = 0; i < n; i++){
// 將所有頂點的顏色着色為白色
color[i] = COLOR.WHITE;
// 所有頂點的父母初始化為-1
parent[i] = -1;
// 所有頂點的深度初始化為無窮大
d[i] = Integer.MAX_VALUE;
f[i] = Integer.MAX_VALUE;
// 節點個訪問次序都初始設為-1
ord[i] = -1;
}
// 調用遞歸函數,進行遍歷訪問
DFS_VISIT(G, start);
}
函數DFS首先對個頂點顏色、兩個時間值以及訪問次序等值進行初始化,然后調用遞歸函數DFS_VISIT對圖中頂點進行深度優先搜索。DFS函數有兩種版本,一種規定搜索的其實頂點,即上面的代碼,這種實現只能搜索到給定頂點能到達的所有頂點;另一種不規定起點,這樣能適用於有分離的子圖構成的圖的遍歷。下面具體討論遞歸遍歷函數DFS_VISIT,函數的實現如下:
* 深度優先搜索算法中調用的遞歸函數,
* 實現圖的遞歸遍歷
* @param G 待搜索的圖
* @param u 搜索的起點
*/
static int k = 0;
public static void DFS_VISIT(Graph G, int u){
// 起點u首先着色為灰色
color[u] = COLOR.GRAY;
// 頂點的訪問時間增加1
time++;
d[u] = time;
// 打印所有頂點的顏色
for( int i = 0; i < G.get_nv(); i++){
System.out.print(i + ")" + color[i] + "\t");
}
System.out.println();
// 打印並顯示每個頂點的深度和父頂點
for( int i = 0; i < G.get_nv(); i++){
String _4print = "";
if(d[i] < Integer.MAX_VALUE
&& f[i] < Integer.MAX_VALUE)
_4print = i + ")" + d[i] + "/" + f[i] + "\t";
else if(d[i] < Integer.MAX_VALUE)
_4print = i + ")" + d[i] + "/" + "\t";
else
_4print = i + ")" + " " + "/" + " " + "\t";
System.out.print(_4print);
}
System.out.println();
// 遞歸搜索頂點u的所有尚未訪問過的相鄰頂點
for(Edge w = G.firstEdge(u);
G.isEdge(w);
w = G.nextEdge(w)){
if(color[w.get_v2()] == COLOR.WHITE){
parent[w.get_v2()] = u;
DFS_VISIT(G, w.get_v2());
}
}
// 將u其顏色改變為黑色
color[u] = COLOR.BLACK;
// 頂點訪問結束時的時間
f[u] = ++time;
// 頂點被訪問的次序
ord[u] = k++;
// 打印各個頂點的顏色
for( int i = 0; i < G.get_nv(); i++){
System.out.print(i + ")" + color[i] + "\t");
}
System.out.println();
// 打印各個頂點被訪問的起始、結束時間
for( int i = 0; i < G.get_nv(); i++){
String _4print = "";
if(d[i] < Integer.MAX_VALUE
&& f[i] < Integer.MAX_VALUE)
_4print = i + ")" + d[i] + "/" + f[i] + "\t";
else if(d[i] < Integer.MAX_VALUE)
_4print = i + ")" + d[i] + "/" + "\t";
else
_4print = i + ")" + " " + "/" + " " + "\t";
System.out.print(_4print);
}
System.out.println();
}
DFS_VISIT函數在執行時首先對本次遞歸調用的時間值time進行更新,將其作為入參頂點u的第一時間d[u];然后將頂點u的顏色重置為灰色。函數關鍵的步驟為遞歸地對搜索頂點u的每一個相鄰頂點,首先搜索第一個尚未被訪問的相鄰點,然后回溯至頂點u並遞歸搜索第二個尚未被訪問的相鄰點。如果頂點的相鄰頂點為白色,則該頂點尚未被訪問。頂點u的所有相鄰頂點都被訪問后將u頂點的着色重置為黑色,並記錄第二個時間f[u]為此時的時間time。
以有向圖圖(b)為例來分析深度優先算法的過程,測試兩種遍歷過程,第一種設定搜索的起點為頂點0,搜索頂點0能到達的所有頂點;第二種不設定頂點,搜索圖中所有頂點。有向圖的邊信息如下:
6
0,1,1
0,3,1
1,4,1
2,4,1
2,5,1
3,1,1
4,3,1
5,5,1
測試示例代碼為:
Utilities.BulidGraphLnkFromFile("Graph\\graph2.txt");
System.out.println("頂點0作為起點深度優先搜索結果:");
GraphSearch.DFS(GL, 0);
System.out.println();
System.out.println("無起始頂點深度優先搜索結果:");
GraphSearch.DFS(GL);
運行結果為:
頂點0作為起點深度優先搜索結果:
0)GRAY1)WHITE2)WHITE3)WHITE4)WHITE5)WHITE
0)1/1) / 2) / 3) / 4) / 5) /
0)GRAY1)GRAY2)WHITE3)WHITE4)WHITE5)WHITE
0)1/1)2/2) / 3) / 4) / 5) /
0)GRAY1)GRAY2)WHITE3)WHITE4)GRAY5)WHITE
0)1/1)2/2) / 3) / 4)3/5) /
0)GRAY1)GRAY2)WHITE3)GRAY4)GRAY5)WHITE
0)1/1)2/2) / 3)4/4)3/5) /
0)GRAY1)GRAY2)WHITE3)BLACK4)GRAY5)WHITE
0)1/1)2/2) / 3)4/54)3/5) /
0)GRAY1)GRAY2)WHITE3)BLACK4)BLACK5)WHITE
0)1/1)2/2) / 3)4/54)3/65) /
0)GRAY1)BLACK2)WHITE3)BLACK4)BLACK5)WHITE
0)1/1)2/72) / 3)4/54)3/65) /
0)BLACK1)BLACK2)WHITE3)BLACK4)BLACK5)WHITE
0)1/81)2/72) / 3)4/54)3/65) /
無起始頂點深度優先搜索結果:
0)GRAY1)WHITE2)WHITE3)WHITE4)WHITE5)WHITE
0)1/1) / 2) / 3) / 4) / 5) /
0)GRAY1)GRAY2)WHITE3)WHITE4)WHITE5)WHITE
0)1/1)2/2) / 3) / 4) / 5) /
0)GRAY1)GRAY2)WHITE3)WHITE4)GRAY5)WHITE
0)1/1)2/2) / 3) / 4)3/5) /
0)GRAY1)GRAY2)WHITE3)GRAY4)GRAY5)WHITE
0)1/1)2/2) / 3)4/4)3/5) /
0)GRAY1)GRAY2)WHITE3)BLACK4)GRAY5)WHITE
0)1/1)2/2) / 3)4/54)3/5) /
0)GRAY1)GRAY2)WHITE3)BLACK4)BLACK5)WHITE
0)1/1)2/2) / 3)4/54)3/65) /
0)GRAY1)BLACK2)WHITE3)BLACK4)BLACK5)WHITE
0)1/1)2/72) / 3)4/54)3/65) /
0)BLACK1)BLACK2)WHITE3)BLACK4)BLACK5)WHITE
0)1/81)2/72) / 3)4/54)3/65) /
0)BLACK1)BLACK2)GRAY3)BLACK4)BLACK5)WHITE
0)1/81)2/72)9/3)4/54)3/65) /
0)BLACK1)BLACK2)GRAY3)BLACK4)BLACK5)GRAY
0)1/81)2/72)9/3)4/54)3/65)10/
0)BLACK1)BLACK2)GRAY3)BLACK4)BLACK5)BLACK
0)1/81)2/72)9/3)4/54)3/65)10/11
0)BLACK1)BLACK2)BLACK3)BLACK4)BLACK5)BLACK
0)1/81)2/72)9/123)4/54)3/65)10/11
結果中每兩行為一個狀態,第一行為個頂點的顏色,第二行為個頂點的兩個時間:d[u]/f[u]。程序運行流程如下:

設定起始頂點為0時,算法只能搜索到圖中頂點0可到達的所有頂點,即頂點1,4,3,其過程為上圖步驟1~8。不設定起始頂點時,可以搜索到圖中所有頂點。
前面我們提到,深度優先搜索所獲得每個頂點的兩個時間d和f蘊含了圖的結構信息,即所謂的“括號對結構”(parenthesis structure)。根據最終得到的各頂點的時間d和時間f,可以得到如圖(a, b)所示的括號結構,其中圖(b)中對各頂點的位置做了調整。

圖 深度優先搜索算法得到的括號對結構
另一方面,根據算法獲得的各頂點的父頂點數組parent[],可以構造深度優先搜索森林。對圖(b)所有頂點進行深度優先搜索得到的父頂點數組為:
-1, 0, -1, 4, 1, 2,
可以構造出如圖所示的深度優先森林。可以發現括號對結構和深度優先森林之間存在對應關系。如果頂點v和頂點u的搜索訪問時間滿足條件d[u]<d[v]<f[v]<d[u],那么頂點v必然是頂點u的后裔。具體證明請讀者參閱相關圖論書籍。

圖 深度優先森林
廣度優先搜索和深度優先搜索算法將多次應用到下面即將介紹的算法中。