第22章:圖的基本演算法—廣度優先搜尋和深度優先搜尋
一:廣度優先搜尋
給定圖G=(V,E)和一個可以識別的源結點s,廣度優先搜尋對圖G的邊進行系統系的探索來發現可以從源結點s到達的所有結點。該演算法能夠計算從源節點s到每個可到達的結點的距離(最少的邊數),同時生成一棵“廣度優先搜尋樹”。該樹以源結點s為根結點,包含所有可以從s到達的結點。對於每個從源結點s可以到達的結點v,在廣度優先搜尋樹裡從結點s到結點v的簡單路徑對應的就是圖G中從結點s到結點v的“最短路徑”,即包含最少邊數的路徑。該演算法既可以用於有向圖,也可以用於無向圖(暗示著該演算法可以用於含有環的圖)。
廣度優先搜尋之所以如此得名是因為該演算法始終是將已發現結點和未發現結點之間的邊界,沿其廣度方向向外擴充套件,也就是說,演算法需要在發現所有距離源節點s為k的所有結點之後,才會發現距離源結點s為k+1的其它結點。
程式碼如下:
//用的是鄰接連結串列表示圖,圖的頂點用0,1,2...這些數字表示。
//下面程式碼用廣度優先搜尋演算法計算源頂點到圖中各頂點的最短距離(即最少邊數)以及對應的路徑。
vector<int> bfs(const vector<list<int>>& graph, int source)
{
enum { WHITE,GRAY,BLACK};
vector<int> color(graph.size(),WHITE);
vector<int> distance(graph.size(),INT_MAX); //存放的是源頂點到圖中各個頂點的最少>邊數;
//比如distance[3]=2表明源頂點vertex到頂點3的最少邊數是2
vector<int> pred(graph.size(),-1); //存放的是對應頂點的前驅結點predecessor;
//比如pred[3]=2表明的是頂點3的前驅結點是頂點2;
color[source]=GRAY;
distance[source]=0;
pred[source]=-1;
queue<int> Q;
Q.push(source);
while (!Q.empty()){
int vertex=Q.front();
Q.pop();
for(auto iter=graph[vertex].begin();iter!=graph[vertex].end();++iter)
{
int tmp=*iter;
if(color[tmp]==WHITE){
color[tmp]=GRAY;
distance[tmp]=distance[vertex]+1;
pred[tmp]=vertex;
Q.push(tmp);
}
}
color[vertex]=BLACK;
}
return pred;
}
//輸出源頂點s到圖中某一個特定頂點v的含有最少邊數的路徑
void print_path(int source,int v,const vector<int>& pred)
{
if(v==source)
cout<<source;
else if(pred[v]==-1)
cout<<"no path from "<<source<<" to "<< v<<endl;
else{
print_path(source,pred[v],pred);
cout<<" to "<<v;
}
}
二:深度優先搜尋
深度優先搜尋正如其名字所表明的,只要可能,就在圖中儘量“深入”。深度優先搜尋總是對最近才發現的結點v的出發邊進行搜尋,直到該結點的所有出發邊都被發現為止。一旦結點v的所有出發邊都被發現,搜尋則回溯到v的前驅結點來搜尋該前驅結點的出發邊,該過程一直持續到從源結點可以到達的所有結點都被發現為止。如果存在尚未被發現的結點,則深度優先搜尋將從這些未被發現的結點中任選一個作為新的源結點,並重復同樣的搜尋過程。該演算法重複整個過程,直到圖中的所有結點都被發現為止。在這裡必須要說明的是,因為廣度優先搜尋通常用來尋找從特定源結點出發的最短路徑距離,而深度優先搜尋則常常作為另一個演算法裡的一個子程式,所以在討論廣度優先搜尋時,源結點的數量限制為一個,而深度優先搜尋則可以有多個源結點。
因為深度優先搜尋會有多個源結點,因此該演算法會形成多棵深度優先樹,這些樹形成了深度優先森林。因為當我們搜尋到一個結點時,我們會標記這個結點被發現了,因此每個結點僅在一棵深度優先樹中出現,因此所有的深度優先樹是不相交的。
除了建立一個深度優先搜尋森林外,深度優先搜尋演算法還在每個結點蓋上一個時間戳。每個結點v有兩個時間戳:第一個時間戳v.d記錄結點v第一次被發現的時間(塗上灰色的時候),第二個時間戳v.f記錄的是搜尋完成對v的鄰接連結串列掃描的時間(塗上黑色的時候)。
深度優先搜尋演算法可以用於有向圖,也可以用於無向圖(暗示著可以用於含有環的圖)。程式碼如下:
void dfs_visit(int source,unsigned int& time, vector<int>& color,vector<int>& pred,
vector<unsigned int>& d,vector<unsigned int>& f,const vector<list<int>>& graph)
{
enum {WHITE,GRAY,BLACK};
time++;
d[source]=time;
color[source]=GRAY;
for(auto iter=graph[source].begin();iter!=graph[source].end();++iter)
{
int tmp=*iter;
if(color[tmp]==WHITE){
pred[tmp]=source;
dfs_visit(tmp,time,color,pred,d,f,graph);
}
else if(color[tmp]==GRAY)
cout<<"邊( "<<source<<", "<<tmp<<" )是後向邊"<<endl;
else{
if(d[source]<d[tmp])
cout<<"邊( "<<source<<", "<<tmp<<" )是前向邊"<<endl;
else
cout<<"邊( "<<source<<", "<<tmp<<" )是橫向邊"<<endl;
}
}
color[source]=BLACK;
time++;
f[source]=time;
}
pair<vector<unsigned int>,vector<unsigned int>> dfs(const vector<list<int>>& graph)
{
enum {WHITE,GRAY,BLACK};
vector<int> color(graph.size(),WHITE);
vector<int> pred(graph.size(),-1);
unsigned int time=0;
vector<unsigned int> d(graph.size()); //記錄的是圖中各個頂點v第一次被發現的時間;
vector<unsigned int> f(graph.size()); //記錄的是搜尋完成對頂點v的鄰接連結串列掃描的時間;
for(int v=0;v!=graph.size();++v) //對圖中每個頂點v進行搜尋;
if(color[v]==WHITE)
dfs_visit(v,time,color,pred,d,f,graph);
return make_pair(d,f);
}
括號化定理:在對有向或無向圖G=(V,E)進行的任意深度優先搜尋中,對於任意兩個節點u和v來說,下面三種情況只有一種成立:
1. 區間[u.d,u.f]和區間[v.d,v.f]完全分離,在深度優先森林中,結點u不是結點v的後代,
結點v也不是結點u的後代;
2. 區間[u.d,u.f]完全包含在區間[v.d,v.f]之內,在深度優先樹中,結點u是結點v的後代;
3. 區間[v.d,v.f]完全包含在區間[u.d,u.f]之內,在深度優先樹中,結點v是結點u的後代。
邊的分類:對於在圖G上執行深度優先搜尋演算法所生成的深度優先森林,我們可以定義4中邊的型別:
1:樹邊:為深度優先森林的邊,如果結點v是因演算法對邊(u,v)的探索而首次被發現的,則(u,v)是一條樹邊;
2:後向邊:後向邊(u,v)是將結點u連線到其在深度優先樹中一個祖先結點v的邊。有向圖可以有自迴圈,自迴圈被認為是後向邊;
3:前向邊:是將結點u連線到其在深度優先樹種一個後代結點v的邊(u,v);
4:橫向邊:指其它所有的邊,這些邊可以連線同一棵深度優先樹中的結點,只要其中一個結點不是另外一個結點的祖先,也可以連線不同深度優先樹中的兩個結點。
深度優先搜尋演算法有足夠的資訊能夠對邊按照上述四種進行分類,這裡的關鍵是,當第一次探索邊(u,v)時,結點v的顏色能夠告訴我們關於邊的一些資訊:
1:結點v為白色表明該條邊(u,v)是一條樹邊;
2:結點v為灰色表明該條邊(u,v)是一條後向邊;
3:結點v為黑色表明該條邊(u,v)是一條前向邊或橫向邊。如果u.d<v.d,則邊(u,v)是前向邊,在u.d>v.d時,(u,v)為橫向邊。
上面的程式碼在對邊進行分類時就利用了上述知識。
在對邊進行分類時,無向圖可能給我們帶來一些模糊性,因為邊(u,v)和邊(v,u)實際上是同一條邊,因此我們可以根據深度搜索演算法是先探索到邊(u,v)還是邊(v,u)來進行分類。根據這個標準,我們會發現:在對無向圖G進行深度優先搜尋時,每條邊要麼是樹邊,要麼是後向邊。
一個有向圖G=(V,E)是無環的當且僅當對其進行深度優先搜尋不產生後向邊。