學習圖論(一)——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。
以上為本人初學圖論的內容,學習過程參考了多篇博文,以後再學深入後若有需要修改之處再修改。