圖的遍歷:DFS和BFS演算法
DFS(深度優先演算法)
1. 原理概述
最基本的DFS是用來解決無向圖的遍歷問題的。無向圖用沿主對角線對稱的鄰接矩陣儲存。DFS演算法最基本的程式碼實現如下:
void dfs(int cur)//cur是當前所在的頂點編號
{
sum++;//每訪問一個頂點,sum+1
if(sum==n) return;
for(i=1;i<=n;i++)//從1號頂點到n號頂點依次嘗試,看哪些頂點與當前頂點cur有邊相連
{
if(e[cur][i]==1 && book[i]==0)
{
book[i]=1 ;//標記頂點i已經訪問過
dfs(i);//從頂點i再出發繼續遍歷
}
}
}
上面,cur儲存當前正在遍歷的頂點,二維陣列e儲存的是圖的邊(鄰接矩陣),book用來記錄哪些頂點已經訪問過,sum用來記錄已經訪問過的頂點個數,n儲存圖中頂點的總個數。
2. 用DFS求所有可能的排列組合
以上是最基本的dfs演算法實現,事實上,程式碼結構絕不是一成不變的。
例如,應用dfs求一串元素的所有可能的排列,抽象出來的圖的分支就十分龐大。我們不關心對整張圖的全部遍歷,而是希望分別輸出每種可能的完整排列順序。這就需要將dfs的步驟改成:
- 定義全域性的用於存放所有解的物件和存放一次解的物件。
- 在遞迴函式中,確定遞迴終止條件(一般為sum==n),並在終止時將一次完整的解存檔。
在遞迴函式中,若沒有終止,程式碼結構應該是:則先執行動作,呼叫遞迴,再撤銷動作。
一個完整的實現程式碼為:
void dfs(int s, vector<int> &nums){
if(s == nums.size()) result.push_back(oneResult);
for(int i = 0; i < nums.size(); i++){
if(used.count(nums[i]) != 0) continue ;//如果元素已經存在,就pass
//執行動作
used.insert(nums[i]);
oneResult.push_back(nums[i]);
dfs(s + 1, nums);
//撤銷動作
oneResult.pop_back();
used.erase(nums[i]);
}
}
其中,名為used的set物件用於儲存已經使用過的元素,類似book陣列。result存放所有可能的結果,而oneResult存放一次完整的排列結果。
3. 用DFS求城市最短路程問題
首先用一個二維矩陣表示任意兩個城市之間的路程。
核心程式碼如下:
void dfs(int cur,int dis)
{
int j;
//如果當前走過的路程已經大於之前找到的最短路,就沒有必要往下嘗試了
if(dis>min) return;
if(cur==n)//如果到達了目標城市
{
if(dis<min) min=dis;//更新最小值
return;
}
for(j=1;j<=n;j++)
{
//判斷當前城市cur到城市j是否有路,並判斷城市j是否已經走過
if(e[cur][j]!=INT_MAX && book[j]==0)
{
book[j]=1;//標記城市j已經走過
dfs(j,dis+e[cur][j]);
book[j]=0;//撤銷對城市j的標記
}
}
}
4 先序遍歷和DFS
對於二叉樹而言,先序遍歷和深度優先遍歷的效果是一樣的。
BFS(廣度優先遍歷)
1. 原理概述
如果需要分層遍歷圖,就需要用到deque資料結構(可以用陣列加兩個輔助指標模擬佇列)。BFS的主要思想是:首先以一個未被訪問過的頂點作為起始頂點,訪問其所有相鄰的頂點,然後對每個相鄰的頂點,再訪問它們相鄰的未被訪問過的頂點,直到所有頂點都被訪問過,遍歷結束。
程式碼實現為:
int book[1001],que[1001],head,tail;//head和end模擬佇列的隊首和隊尾位置
head=1;
tail=1;
//從1號頂點出發,將1號頂點加入佇列
que[tail]=1;
tail++;
book[1]=1;//標記1號頂點已訪問
//BFS核心部分
while(head<tail)//當佇列不為空的時候迴圈
{
cur=que[head];
for(i=1;i<=n;i++)
{
//判斷從頂點cur到頂點i是否有邊,並判斷頂點i是否已經被訪問過
if(e[cur][i]!=INT_MAX && book[i]==0)
{
//將頂點i加入佇列
que[tail]=i;
tail++;
book[i]=1;
}
//如果tail大於n,說明所有頂點都被訪問過
if(tail>n)
{
break;
}
}
//當一個頂點拓展結束後,要將head++,相當於從隊首刪除該頂點
head++;
}
2. 用BFS解最少轉機問題
假設有當前城市和目標城市,兩者沒有直航,但有和其他城市的航班往來。希望找到一種最少轉機的方法。
這個問題中,我們不關心兩個城市間的往來時間(邊的權值),如果可以往來,一律視為有權值為1的邊。我們關心從城市A到B經過的邊的數量最少。這也是DFS和BFS的不同應用之處。
核心程式碼如下:
while(head<tail)
{
cur=que[head].x;//que是一個結構體陣列,每個元素是一個結構體,x表示城市編號,s表示轉機次數
for(j=1;j<=n;j++)
{
if(e[cur][j]!=INT_MAX && book[j]==0)//從cur到j是否有航班,並且判斷j是否已經經過
{
que[tail].x=j;
que[tail].s=que[head].s+1;//轉機次數+1
tail++;
book[j]=1;
}
//如果已經到達目標城市
if(que[tail].x==end)
{
flag=1;
break;
}
}
//當一個點拓展完後,head++
head++;
}
DFS和BFS演算法的區別
從前面分別舉的幾個例子可以看出,DFS和BFS應用場景並不相同。
當我們關心一條路徑的權重合時,適合用DFS演算法。
當我們關心一條路徑的邊的數量時,適合用BFS演算法。