1. 程式人生 > >學習圖論(一)——DFS與BFS

學習圖論(一)——DFS與BFS

一、圖的基本要素
1.頂點/節點(vertex);
2.邊(edge),連線兩個點,可以為無向邊也可以為有向邊,可以為有權邊也可以為無權邊;
3.連通圖:圖上的任意兩個點之間都是連通的。 即是任意兩個點都有一條或者多條邊連線著。
4.連通分量:最大連通子圖。即是①是該圖的子圖;②該子圖是連通的;③是含節點數最多的子圖。

1、深度優先搜尋(DFS)

思想:顧名思義,深度優先,就是從一個頂點開始,往下搜尋與該頂點相鄰的節點,一直搜到所搜的節點不存在相鄰節點,然後開始回溯,往回走,一次回到上一個節點,搜尋其他路徑,知道遍歷所有的節點或者找到解為止。

核心程式碼:
void DFS(int cur)// cur為當前的點
{
if(邊界條件或者需要判斷的條件) //條件可以有多個,即有多個 if 語句
{
執行所需的操作;
return ;
}
做需要執行的操作;
如在走一張圖時要分別搜尋上下左右四個方向,則有一個for迴圈,每次走一個方向
//執行完應有的操作後,選擇符合條件的節點繼續進行搜尋。
DFS(符合條件的下一個節點);
}

簡單例題
輸入n個結點,m條邊,之後輸入 有向圖的 m條邊,
邊的前兩元素表示起始結點,第三個值表權值,
輸出1號城市到n號城市的最短距離。
AC程式碼:

#include<iostream>
#include<cstring>
using namespace std;
#define inf 999999999
#define maxn 110
//minpath 最短路徑,因為用DFS,每個點都要搜尋,所搜尋完一條路徑,都記錄下來,用於比較; 
//egde宣告為二維陣列,表示兩個有向連線點之間的距離(能表示雙向),但是資料大時不可以用;
//mark作為標記作用,標記以訪問過的節點;
int n,m,minpath,egde[maxn][maxn],mark[maxn]; void dfs(int cur,int dist) //當前點, 當前距離 { if(dist>minpath) //在當前點就有距離大於最小路程,下面不用再遍歷 return ; if(cur==n) // 已經到終點 { if(minpath>dist) //求最短距離 minpath = dist; return ; } for(int i=1;i<=n;++i) { //條件分別為:兩點間有邊,下一個點還沒訪問過,兩個點不是同一個點,因為同一點的話沒必要進行判斷
if(egde[cur][i]!=inf&&!mark[i]&&egde[cur][i]) { mark[i]=1; dfs(i,dist+egde[cur][i]);//重新整理當前距離距離 mark[i]=0; //回溯,因為每條路徑都要搜尋一遍,可能有重複經過的點 } } return ; } int main() { while(cin>>n>>m&&n) { int i,j; for(int i=1; i<=n; ++i) { for(int j=1; j<=n; ++j) egde[i][j] = inf; //把每條邊賦做無窮,再輸入邊長,則無窮表示的是兩個節點無連線 egde[i][i]=0; //同一點距離為 0 } int a,b;//表前後兩個節點 while(m--) //輸入邊長 { cin>>a>>b; cin>>egde[a][b]; } memset(mark,0,sizeof(mark));//將標記陣列全置為 0 ,標頭檔案為 <cstring> mark[1]=1; minpath=inf; dfs(1,0); cout<<minpath<<endl; } }

總而言之,用DFS,就是每個點你都要變遍歷(因為我們不知道需要遍歷幾個點才能找到答案,所以一開始當然是準備要遍歷所有的點),所以為了防止在每一條路上遍歷時會訪問已經訪問過的點,我們需要把訪問過的點做標記,當一條路走完後,再把每個標記過的點重設為未標記,因為遍歷另一條路可能會經過上一條路經過的點。
使用DFS發生TLE(超時)時一般是邊界條件出錯了,或者沒有標記導致迴圈訪問。(想象三個點互相連線,要訪問這三個點,如果沒有標記已經訪問過的點,則會無盡迴圈下去)發生WA(答案錯誤),可能是沒有重置標記。

2.廣度優先搜尋(BFS)
思想:廣度,同寬度,也就是先搜尋所有和當前的節點相鄰的點,然後在往下一層搜尋,發現目標或者達到目的時停止搜尋。
和DFS不同,由於BFS需要先變遍歷所有相鄰的點,所以無法用遞迴去實現,而是使用佇列這一資料結構——先入先出。

核心程式碼:
void BFS(data_type u)
{
queue< 資料型別> q; // 建立一個佇列 佇列
定義當前狀態;
q.push(u); // 開始的節點入隊
visit[u]=1; // 訪問標記
while(!q.empty())//佇列空時退出,此時已經有答案或者無解
{
now=q.front(); // 取隊首元素進行擴充套件
//if 和 for 的順序可以顛倒,視情況而定
if(到達目標條件)
{
進行相應操作;
return;
}
執行需要的操作,依次訪問與隊首元素相鄰的點;
找到後進行標記,將符合條件的點放入佇列中;
隊首元素出出隊,因為已經訪問過並且執行過需要的操作了。
}
return;
}

簡單例題:
建立一個簡單的5×5的迷宮,0表示可以走的路,1表示障礙物,不可走。
只能往上下左右四個方向行走,請找出從左上角到右下角的最短路線。
(輸出依次經過的座標)

#include<iostream>
#include<queue>
using namespace std;

//BFS例項 

struct path{
    int curx; //當前座標
    int cury;
    int prex; //所走路線上前一個點的座標
    int prey;
}move[5][5];
int maze[5][5];//迷宮
int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}};//四個不同的方向
int vit[5][5]={0};//標記陣列,記錄節點是否已經被訪問
bool check(int x,int y) //檢查節點是否滿足可以繼續搜尋的條件
{
    if(0<=x&&x<5&&0<=y&&y<5&&!vit[x][y]&&!maze[x][y])
        return true;
    return false;
}
void print(int x,int y)//輸出答案的函式,用了DFS的思想
{
    if((x==0)&&(y==0))//從起點開始輸出
        cout<<'('<<x<<','<<y<<')'<<endl;
    else
    {
        //當前座標不是起點座標,則繼續搜尋當前座標的前一個座標,直到為起點座標
        print(move[x][y].prex,move[x][y].prey);
        cout<<'('<<x<<','<<y<<')'<<endl;
        return ;
    }
}
void BFS(int stax,int stay)
{
    queue<path> q;
    struct path now;//當前狀態
    int tx,ty;
    move[stax][stay].curx=stax;
    move[stax][stay].cury=stay;
    q.push(move[0][0]);//起始點入隊
    vit[0][0]=1;//已經入隊,將被訪問,標記為1
    while(!q.empty())
    {
        now = q.front();    
        for(int i=0;i<4;++i)//依次走與當前點相鄰的四個方向        
        {
            //更新節點狀態
            tx=now.curx+dir[i][0];
            ty=now.cury+dir[i][1];
            if(check(tx,ty))
            {
                //相鄰節點滿足情況,則記錄該節點的前一個節點為隊首節點
                move[tx][ty].prex=now.curx;
                move[tx][ty].prey=now.cury;
                vit[tx][ty]=1;
                q.push(move[tx][ty]);//滿足情況的節點入隊
            }
            if(tx==4&&ty==4)//到達目的地
            {
            print(tx,ty);
            return ;
            }
        }
        q.pop();//隊首元素出隊,因為已經訪問過了。
    }
    return ;
}
int main()
{
    for(int i=0;i<5;++i)
        for(int j=0;j<5;++j)
        {
            cin>>maze[i][j];
            move[i][j].curx=i;
            move[i][j].cury=j;
        }
    BFS(0,0);
}

BFS常用於求最短路問題,它是按照一層一層的訪問,一層上的節點再對應下一層的相連線的點,所以BFS需要的記憶體比較大。
而DFS常用於求連通性問題,判斷一個圖是否連通,先一條路走到死,再回到起點換一條路走。DFS其實類似於棧(stack)。因為它需用多層遞迴,所以容易TLE。

以上為本人初學圖論的內容,學習過程參考了多篇博文,以後再學深入後若有需要修改之處再修改。