# DFS與BFS ## 一、Depth-first search 深度優先搜尋法 (DFS) ### 1.簡述 DFS是一種用來搜尋一個數或圖的演算法,每當走到一個節點,就會以那個節點為新起始點,往其中一邊搜尋到到底或下一個節點。當已經走遍節點其中一邊的所有可能,才會開始走節點的另外一邊。 ### 2.圖例 ![](https://i.imgur.com/syqT8fQ.png) 從上面的樹狀圖可以看出來,DFS的搜尋方式是遇到節點時,會先選擇把某一邊的所有可能路徑走完,才會去走另外一邊。 ## 二、Breadth-First Search 廣度優先搜尋 (BFS) ### 1.簡述 BFS也是一種用來搜尋一個數或圖的演算法,但不同於DFS的是遇到節點時,會同時往節點的各個方向走相同的單位搜尋,不像DFS會優先走完其中一邊才走另一邊。 ### 2.圖例 ![](https://i.imgur.com/8vZ1ZsF.png) 從上面的樹狀圖及迷宮可以看出來,BFS的搜尋方式是遇到節點時,會同時往岔路的各個方向搜尋。 ## 三、迷宮問題實作-程式碼 以下的C++程式碼將會利用DFS及BFS的方式走由0跟1組成的迷宮(迷宮的常跟寬小於100),0代表路,1代表牆,如下圖所示。 目標是從迷宮的左上角走到最右下角,走過的路不能重複走,並求出最短距離以及路徑。 ![](https://i.imgur.com/Yx8vhe5.jpg =300x) ### 1.DFS #### (1)列出一條路徑 ```c++ #include <iostream> #include <utility> #include <deque> #define N 100 using namespace std; int map[N][N], visited[N][N]={0}; typedef pair<int, int> p; int n,m,found=0; deque<p> path; void dfs(int x, int y){ if (found==1) return; visited[x][y]=1; path.push_back(make_pair(x,y)); if (x==n-1 && y==m-1){ found=1; cout<<"Path: "; while(!path.empty()){ cout<<"("<<path.front().first<<","<<path.front().second<<")"; path.pop_front(); cout<<((path.empty())?"\n":"→"); } cout<<endl; return; } if (x+1<n && visited[x+1][y]==0 && map[x+1][y]==0){ dfs(x+1,y); path.pop_back(); } if (y+1<m && visited[x][y+1]==0 && map[x][y+1]==0){ dfs(x,y+1); path.pop_back(); } if (x-1>=0 && visited[x-1][y]==0 && map[x-1][y]==0){ dfs(x-1,y); path.pop_back(); } if (y-1>=0 && visited[x][y-1]==0 && map[x][y-1]==0){ dfs(x,y-1); path.pop_back(); } } int main(){ cin>>n>>m; for (int i=0; i<n; i++) for (int j=0; j<m; j++) cin>>map[i][j]; dfs(0,0); if (found==0){ cout<<"No routes accessible.\n"; } return 0; } ``` #### (2)顯示最短距離 ```c++ #include <iostream> #include <utility> #include <deque> #define N 100 using namespace std; int map[N][N], visited[N][N]={0}; typedef pair<int, int> p; int n,m,dis=-2; deque<p> path; void dfs(int x, int y){ visited[x][y]=1; path.push_back(make_pair(x,y)); if (x==n-1 && y==m-1){ if (dis==-1){ dis=path.size()-1; } else { if (path.size()-1<dis) dis=path.size()-1; } } if (x+1<n && visited[x+1][y]==0 && map[x+1][y]==0){ dfs(x+1,y); visited[x+1][y]=0; path.pop_back(); } if (y+1<m && visited[x][y+1]==0 && map[x][y+1]==0){ dfs(x,y+1); visited[x][y+1]=0; path.pop_back(); } if (x-1>=0 && visited[x-1][y]==0 && map[x-1][y]==0){ dfs(x-1,y); visited[x-1][y]=0; path.pop_back(); } if (y-1>=0 && visited[x][y-1]==0 && map[x][y-1]==0){ dfs(x,y-1); visited[x][y-1]=0; path.pop_back(); } } int main(){ cin>>n>>m; for (int i=0; i<n; i++) for (int j=0; j<m; j++) cin>>map[i][j]; dfs(0,0); if (dis==-2) cout<<"No routes accessible.\n"; else cout<<"Shortest distance: "<<dis<<endl; return 0; } ``` #### (3)列出所有路徑 ```c++ #include <iostream> #include <utility> #include <deque> #define N 100 using namespace std; int map[N][N], visited[N][N]={0}; typedef pair<int, int> p; int n,m,idx=1,found=0; deque<p> path; void dfs(int x, int y){ visited[x][y]=1; path.push_back(make_pair(x,y)); if (x==n-1 && y==m-1){ found=1; cout<<"Path "<<idx<<":\n"; idx++; int len=path.size(); for (int i=0; i<len; i++){ cout<<"("<<path[i].first<<","<<path[i].second<<")"; cout<<((i==len-1)?"\n":"→"); } return; } if (x+1<n && visited[x+1][y]==0 && map[x+1][y]==0){ dfs(x+1,y); visited[x+1][y]=0; path.pop_back(); } if (y+1<m && visited[x][y+1]==0 && map[x][y+1]==0){ dfs(x,y+1); visited[x][y+1]=0; path.pop_back(); } if (x-1>=0 && visited[x-1][y]==0 && map[x-1][y]==0){ dfs(x-1,y); visited[x-1][y]=0; path.pop_back(); } if (y-1>=0 && visited[x][y-1]==0 && map[x][y-1]==0){ dfs(x,y-1); visited[x][y-1]=0; path.pop_back(); } } int main(){ cin>>n>>m; for (int i=0; i<n; i++) for (int j=0; j<m; j++) cin>>map[i][j]; dfs(0,0); if (found==0){ cout<<"No routes accessible.\n"; } return 0; } ``` ### 2.BFS #### (1)列出一條路徑 ```c++ #include <iostream> #include <utility> #include <deque> #include <cstring> #define N 100 using namespace std; int map[N][N]; typedef pair<int, int> p; int n,m,routes=1; deque<p> path; p pre[N][N]; int nxt[4][2] = {{-1,0},{0,1},{1,0},{0,-1}}; void bfs(int x, int y){ path.push_back(make_pair(x,y)); pre[x][y].first=x; pre[x][y].second=y; while (path.front().first!=n-1 || path.front().second!=m-1){ if (routes==0) return; p from=path.front(); path.pop_front(); for (int i=0; i<4; i++){ p to; to.first=from.first+nxt[i][0]; to.second=from.second+nxt[i][1]; if (to.first<0 || to.first>=n || to.second<0 || to.second>=m) continue; if (map[to.first][to.second] || pre[to.first][to.second].first != -1) continue; routes++; path.push_back(to); pre[to.first][to.second].first=from.first; pre[to.first][to.second].second=from.second; } routes--; } } void print(int x, int y){ if(pre[x][y].first != x || pre[x][y].second != y) print(pre[x][y].first, pre[x][y].second); cout<<"("<<x<<","<<y<<")"; cout<<((x==n-1&&y==m-1)?"\n":"→"); } int main(){ cin>>n>>m; for (int i=0; i<n; i++) for (int j=0; j<m; j++) cin>>map[i][j]; memset(pre,-1,sizeof(pre)); bfs(0,0); if (routes==0) cout<<"No routes accessible.\n"; else{ cout<<"Path: "; print(n-1,m-1); } return 0; } ``` #### (2)顯示最短距離 ```c++ #include <iostream> #include <utility> #include <deque> #include <cstring> #define N 100 using namespace std; int map[N][N]; typedef pair<int, int> p; int n,m,dis=0,routes=1; deque<p> path; p pre[N][N]; int nxt[4][2] = {{-1,0},{0,1},{1,0},{0,-1}}; void bfs(int x, int y){ path.push_back(make_pair(x,y)); pre[x][y].first=x; pre[x][y].second=y; while (path.front().first!=n-1 || path.front().second!=m-1){ if (routes==0) return; p from=path.front(); path.pop_front(); for (int i=0; i<4; i++){ p to; to.first=from.first+nxt[i][0]; to.second=from.second+nxt[i][1]; if (to.first<0 || to.first>=n || to.second<0 || to.second>=m) continue; if (map[to.first][to.second] || pre[to.first][to.second].first != -1) continue; routes++; path.push_back(to); pre[to.first][to.second].first=from.first; pre[to.first][to.second].second=from.second; } routes--; } } void print(int x, int y){ if(pre[x][y].first != x || pre[x][y].second != y){ print(pre[x][y].first, pre[x][y].second); dis++; } } int main(){ cin>>n>>m; for (int i=0; i<n; i++) for (int j=0; j<m; j++) cin>>map[i][j]; memset(pre,-1,sizeof(pre)); bfs(0,0); print(n-1,m-1); if (routes==0) cout<<"No routes accessible.\n"; else cout<<"Shortest distance: "<<dis<<endl; return 0; } ``` ### 3.附註 實際上在測跑程式碼的時候我還讓它印出迷宮的圖及座標方便觀看。 印圖的程式碼如下: ```c++ #include <iomanip> void show(){ cout<<endl<<" y"; for (int i=0; i<m; i++){ cout<<" "<<setfill('\0')<<setw(2)<<i; } cout<<endl<<" x "; for (int i=0; i<3*m+1; i++){ cout<<"-"; } cout<<endl; for (int i=0; i<n; i++){ cout<<setfill('\0')<<setw(2)<<i<<" |"; for (int j=0; j<m; j++){ cout<<" "<<map[i][j]; } cout<<endl; } cout<<endl; } ``` 有很多細節是為了美觀打的,像是setfill、setw是為了讓數字對齊。 ## 四、迷宮問題實作-結果及說明 ### 1.正常測資 ```c++ 6 8 0 0 0 0 0 0 1 1 1 0 1 0 1 0 0 0 0 0 1 1 1 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 1 1 0 1 1 0 1 0 0 0 0 0 ``` 以下結果將會為以上面的數值當作輸入 #### (1)列出一條路徑(DFS&BFS) ##### DFS: ![](https://i.imgur.com/5u8wAWx.jpg) ##### BFS: ![](https://i.imgur.com/SJ44mbP.jpg) 從以上可以看出兩者顯示的是不同到達終點的路徑。 DFS的作法中,遇到岔路的時候,會根據我設定優先行進的方向繼續走到底,走到終點馬上回傳,因此回傳的不一定是距離最短的路徑。 BFS的作法中,遇到岔路的時候,會同時往各個方向走相同的單位,當其中一邊走到終點時馬上回傳,因次回傳的路徑即是走完迷宮的最短路徑。 #### (2)顯示最短距離(DFS&BFS) ![](https://i.imgur.com/E1oW1n6.jpg) 因為用DFS跟BFS印出來的結果長得一模一樣,因此就只放一張圖。 DFS的作法中,因為只印出一條路徑的結果不一定是最短距離,因此我讓它跑完所有可能的路徑,再找出其中得最短路徑算出最短距離。 BFS的作法中,因為回傳的路徑已經是最短距離,所以利用這條路徑算出最短距離即可。 #### (3)列出所有路徑(DFS) ![](https://i.imgur.com/700YXYx.jpg =2000x) 利用DFS,我讓它用遞迴跑完走完迷宮的所有可能路徑。 至於BFS比較麻煩,在我能力範圍內還想不出來要怎麼做QAQ ### 2.走不出去的迷宮 ```c++ 6 8 0 0 0 0 0 0 1 1 1 0 1 0 1 0 0 0 0 0 1 1 1 0 1 0 0 1 1 0 0 1 0 1 0 0 1 0 1 1 0 1 1 0 1 0 0 0 0 0 ``` 這裡我對上面第一組測資做了一些改變,讓迷宮變成無解。 #### 結果 ![](https://i.imgur.com/bNLmfn8.jpg) 很顯然五個程式碼的結果印出來會長一模一樣,因此一樣省略其他圖。至於如何判斷無解兩種方法不太一樣。 DFS的作法中,必須把每一種路徑跑完,確定每一種路徑都無法到達終點,才可以確定無解。 BFS的作法中,我是讓它在走迷宮的同時記錄每一刻還有多少路能走,因此當某一刻判斷出來已經沒有路能走,且沒有回傳任何一條能走到終點的路徑,即代表這個迷宮無解。 ### 3.超大迷宮 ``` 15 28 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 1 1 1 1 1 1 0 1 0 0 1 0 0 1 1 1 1 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 1 1 1 1 1 0 0 0 1 0 1 0 0 1 1 1 1 1 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 1 1 1 1 1 1 0 1 0 0 1 0 0 1 1 1 1 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 ``` 接下來這組測資是一條超大的迷宮,並且非常極端的測資,兩格寬的路根本折磨人。 #### (1)列出一條路徑(DFS&BFS) ##### DFS ![](https://i.imgur.com/aEb38Xv.jpg) ##### BFS ![](https://i.imgur.com/fGrBRhC.jpg) 這個測資對DFS跟BFS來說都還不算太困難,但如果測資設計的非常擊敗,DFS可能會跑稍微久一點,因為DFS一次只跑一邊,而BFS是每一邊都跑。 從上面的結果又可以再次驗證DFS回傳的路徑不一定是最短的,但BFS回傳的路徑即是最短路徑。 #### (2)顯示最短距離(DFS&BFS) ![](https://i.imgur.com/UsRc35d.jpg) 理論上兩個程式跑出來都要是同個結果,但對DFS來說,這個測資要跑太久了,因為DFS會先把所有路徑都跑完再找最短路徑。但其實會造成這個測資要跑太久的主要原因是兩格寬的路,會增加太多可能的路徑。如果有太多莫名其妙的岔路可能也會造成它跑比較久。 相反的,這個測資對BFS來說根本輕輕鬆鬆,因為它是每一邊同時跑,最先回傳的路徑即是最短路徑。 ```c++ 15 28 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 0 1 0 1 1 1 0 1 1 1 1 1 0 1 1 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 1 1 0 0 0 0 0 0 1 0 1 1 0 1 0 1 1 1 1 1 1 0 0 0 1 0 1 0 0 1 1 1 1 1 0 1 1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 0 1 0 1 0 1 1 0 1 1 1 0 1 1 1 1 1 1 0 1 1 0 1 1 0 1 1 1 1 1 0 1 0 1 1 0 1 0 1 0 0 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 ``` 基於好奇,我又改了一下測資的迷宮,把兩格寬的路變成一格寬,測出來DFS就還是可以在短時間跑完。 同時,測出來的最短距離一樣是47,因為改變的路寬不至於影響最短距離。 #### (3)列出所有路徑(DFS) ![](https://i.imgur.com/chwUFJH.jpg) 要用DFS跑完這個非常極端的測資真的要花太多時間,因為至少有幾萬條路徑,小弟我就附上正在跑結果的某一段畫面吧orz。 (我還很有耐心的等到一萬條路徑以上 ![](https://i.imgur.com/Ar2VMhJ.jpg) 這是用上面簡化過、把路壓縮過後的測資跑出來的結果,因為還是有不少可能的路徑,就只附上終止後的結果吧。 ## 五、結論 經過以上的迷宮實作,我深刻地了解到DFS及BFS適合用在不一樣的功能。雖然對比較小或比較單純的測資來說,DFS跟BFS都能在短時間內跑完,但對比較大或複雜的測資就能看出差異。 1. 如果是要求出最短路徑,BFS效率高很多。因為BFS只要一直擴大範圍搜尋,先到終點回傳的路徑即是最短路徑;相反地,DFS需要跑完所有路徑才能求出最短路徑。 2. 如果是要求出其中一條路徑,BFS的穩定度稍微高一點。因為如果測資設計的非常複雜,DFS可能會跑稍微久一點,但BFS比較不會被測資的擊敗程度影響,因為它是同時往四面八方搜尋。 3. 如果是要窮舉出所有可能路徑,DFS則比較適合。雖然我自己也寫不出BFS的窮舉法,但我感覺BFS的演算法本來就不適合做這類的窮舉,DFS則比較適合暴力搜尋。 4. 其實BFS還有一個致命的缺點,那就是會耗費太多記憶體,原因出在BFS可能會花很多記憶體把各個分散開來的點的位置存進陣列。 #### 總結:DFS跟BFS各有優缺點,在一般的情況用DFS或BFS都可以。BFS耗記憶體,但如果DFS的樹過長,可以考慮用BFS;如果要求出最佳解,用BFS比較好;如果要窮舉,用DFS會比較好。遇到問題的時候,應該具體去分析,選擇要用哪種演算法。