1. 程式人生 > >圖的遍歷:DFS和BFS演算法

圖的遍歷: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的步驟改成:

  1. 定義全域性的用於存放所有解的物件和存放一次解的物件。
  2. 在遞迴函式中,確定遞迴終止條件(一般為sum==n),並在終止時將一次完整的解存檔。
  3. 在遞迴函式中,若沒有終止,程式碼結構應該是:則先執行動作,呼叫遞迴,再撤銷動作。

    一個完整的實現程式碼為:

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演算法。