DFS與BFS——理解簡單搜尋(中文虛擬碼+例題)
新的方法和概念,常常比解決問題本身更重要。————華羅庚
引子
深度優先搜尋(Deep First Search) 廣度優先搜尋(Breath First Search) 當菜鳥們(比如我)初步接觸演算法的時候,會接觸這兩種簡單的盲目搜尋演算法,相較與其他眾多的演算法,這兩種演算法相對較好理解,運用範圍也很廣,在眾多的學科競賽裡都可以見到它們的影子,話不多說,我們開始。
深度優先搜尋(Deep First Search)
深度優先搜尋演算法(Depth First Search):一種用於遍歷或搜尋樹或圖的演算法。 沿著樹的深度遍歷圖的節點,儘可能深的搜尋圖的分支。當節點v的所在邊都己被探尋過或者在搜尋時結點不滿足條件,搜尋將回溯到發現節點v的那條邊的起始節點。整個程序反覆進行直到所有節點都被訪問為止。屬於盲目搜尋,最糟糕的情況演算法時間複雜度為O(!n)。
做一個形象的比喻,dfs好比走迷宮,得一直走到頭,看看路的盡頭是不是出口,如果是,就直接走出去,如果不是,那就返回上一個“標記點”尋找不一樣的可行方法。 dfs的實現關鍵在於回溯,這個可以用兩種方法實現(遞迴、堆疊),以下給出虛擬碼
遞迴實現:
遞迴實現是dfs最廣泛的使用方法。
void dfs(int x,int y) { if(達到出口||無法繼續) { 相應操作; return; } if(對應x方向的下一步可以繼續) { 新增標記;//給該位置記上標記,如果後續遞迴呼叫碰到了這個點,則該方向不能繼續下一步 dfs(x+1,y);//呼叫遞迴 取消標記;//上一步對應的遞迴操作全部結束,則要取消標記對後續操作的影響 } else if(對應y方向的下一步可以繼續){ 新增標記; dfs(x,y+1); 取消標記; } }
棧實現:
棧實現的基本思路是將一個節點所有未被訪問的“鄰居”(即“一層鄰居節點”)壓入棧中“待用”,然後圍繞頂部節點進行判定,每個節點被訪問後被踹出,為了程式碼的簡潔易懂,使用了c++的stl。
void dfs_stack(int start, int n) { stack <element_type> s;//建立棧 for (int i=0;i<n;i++) { if (下一步可走&&該點未被標記/排除) { 標記訪問; s.push(i);//入棧 } } while (!s.empty()) {//如果棧非空 訪問s.top()(棧頂); 相應操作; s.pop();//出棧 for (int i = 1; i <= n; i++) { if ((棧頂的)下一步可走&&該點未被標記) { 標記訪問 s.push(i);//入棧 } } } }
例題理解 洛谷 P2392 kkksc03考前臨時抱佛腳:
題目背景
kkksc03 的大學生活非常的頹廢,平時根本不學習。但是,臨近期末考試,他必須要開始抱佛腳,以求不掛科。題目描述
這次期末考試,kkksc03 需要考 4 科。因此要開始刷習題集,每科都有一個習題集,分別有 s1,s2,s3,s4 道題目,完成每道題目需要一些時間,可能不等。 kkksc03 有一個能力,他的左右兩個大腦可以同時計算 2道不同的題目,但是僅限於同一科。因此,kkksc03 必須一科一科的複習。 由於 kkksc03 還急著去處理洛谷的 bug,因此他希望儘快把事情做完,所以他希望知道能夠完成複習的最短時間。輸入格式
本題包含 5 行資料:第 1 行,為四個正整數 s1,s2,s3,s4。 第 2 行,為A1,A2,…,As1 共s1 個數,表示第一科習題集每道題目所消耗的時間。 第 3 行,為 B1,B2,…,Bs2 共s2 個數。 第 4 行,為 C1,C2,…,Cs3 共s3 個數。 第 5 行,為 D1,D2,…,Ds4 共 s4 個數,意思均同上。輸出格式
輸出一行,為複習完畢最短時間。輸入輸出樣例
輸入1 2 1 3 5 4 3 6 2 4 3輸出
20
AC程式碼
#include<bits/stdc++.h>
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;
int Left, Right, minn, ans=0;
int s[5];
int a[21][5];
void dfs(int x, int y) {
if (x > s[y]) {//任務全部分配完畢,做最後處理
minn = min(minn, max(Left, Right));
return;
}
Left += a[x][y];//任務丟給左腦(標記)
search(x + 1, y);
Left -= a[x][y];//把左腦的任務抽出給右腦(取消標記)
Right += a[x][y];//任務丟給右腦(標記)
search(x + 1, y);
Right -= a[x][y];//把右腦的任務抽出給左腦(取消標記)
}
int main() {
cin >> s[1] >> s[2] >> s[3] >> s[4];
for (int i = 1; i <= 4; i++) {//減少碼量
Left = Right = 0;
minn = INF;
for (int j = 1; j <= s[i]; j++)
cin >> a[j][i];
dfs(1, i);//分別對4門科目深搜;
ans += minn;
}
cout << ans;
return 0;
}
本程式碼部分摘自洛谷題解
總結:
DFS為很多多種情況的問題提供了了一個不太用動腦子的解決方案,對於一些可以暴力解決的搜尋,全排列案例有一些參考價值。
廣度優先搜尋(Breath First Search)
廣度優先搜尋(Breath First Search):屬於一種盲目搜尋法,目的是系統地展開並檢查圖中的所有節點,以找尋結果。換句話說,它並不考慮結果的可能位置,徹底地搜尋整張圖,直到找到結果為止。因為所有節點都必須被儲存,因此BFS的空間複雜度為 O(|V| + |E|),其中 |V| 是節點的數目,而 |E| 是圖中邊的數目。最差情形下,時間複雜度為 O(|V| + |E|),其中 |V| 是節點的數目,而 |E| 是圖中邊的數目。
再來一個形象的比喻,你是一個高度近視者,有一次起床,你的眼鏡找不到了,於是你就在你四周摸索,直到慢慢摸出你的眼鏡。 BFS一般用佇列實現,因為佇列先進先出的模式很契合bfs的判定方式——先遍歷所有可行的下一步,再放入隊中,在從隊頂一個一個的判定結果,迴圈執行直到得到結果。
實現:
#include<queue>//stl的queue容器
queue<element_type> qu;//建立佇列
void bfs(起始狀態){
while(未到達最終狀態){
if(起始狀態向x方向可走)
qu.push(起始狀態+x);//該狀態入隊
if(起始狀態向y方向可走)
qu.push(起始狀態+y);//該狀態入隊
…………………
while(!qu.empty()){//當佇列非空
處理(隊頂)qu.top();
相應操作;
qu.pop();//隊首彈出隊
}//一次迴圈結束,執行下一次迴圈
}
}
例題理解 OpenJ_Bailian - 2790 迷宮
題目背景
一天Extense在森林裡探險的時候不小心走入了一個迷宮,迷宮可以看成是由n * n的格點組成,每個格點只有2種狀態,.和#,前者表示可以通行後者表示不能通行。同時當Extense處在某個格點時,他只能移動到東南西北(或者說上下左右)四個方向之一的相鄰格點上,Extense想要從點A走到點B,問在不走出迷宮的情況下能不能辦到。如果起點或者終點有一個不能通行(為#),則看成無法辦到。
Input
第1行是測試資料的組數k,後面跟著k組輸入。每組測試資料的第1行是一個正整數n (1 <= n <= 100),表示迷宮的規模是n * n的。接下來是一個n * n的矩陣,矩陣中的元素為.或者#。再接下來一行是4個整數ha, la, hb, lb,描述A處在第ha行, 第la列,B處在第hb行, 第lb列。注意到ha, la, hb, lb全部是從0開始計數的。
Output
k行,每行輸出對應一個輸入。能辦到則輸出“YES”,否則輸出“NO”。
Sample Input
2 3 .## ..# #.. 0 0 2 2 5 ..... ###.# ..#.. ###.. ...#. 0 0 4 0
Sample Output
YES NO
AC程式碼
#include<bits/stdc++.h>
#define mod 1000000007
#define eps 1e-6
#define ll long long
#define INF 0x3f3f3f3f
#define MEM(x,y) memset(x,y,sizeof(x))
using namespace std;
int T,n,m;
int sx,sy,ex,ey;//初始位置 結束位置
char mp[1005][1005];//原始地圖
int dt[][2]= {{1,0},{-1,0},{0,1},{0,-1}};//方向
struct node
{
int x,y;//橫縱座標
};
node now,net;
void bfs()
{
int f=0;
queue<node>q;
now.x=sx,now.y=sy;
mp[now.x][now.y]='#';//這裡走過 變'.'為'#'即可
q.push(now);
while(!q.empty())
{
now=q.front();
q.pop();
if(now.x==ex&&now.y==ey)//到達終點
{
f=1;
cout<<"YES"<<endl;
break;
}
for(int i=0; i<4; i++)
{
net.x=now.x+dt[i][0];
net.y=now.y+dt[i][1];
if(net.x>=0&&net.x<n&&net.y>=0&&net.y<n&&mp[net.x][net.y]=='.')
{
q.push(net);
mp[net.x][net.y]='#';//這裡走過 變'.'為'#'即可
}
}
}
if(f==0)
{
cout<<"NO"<<endl;
return;
}
}
int main()
{
cin>>T;
while(T--)
{
cin>>n;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
cin>>mp[i][j];
cin>>sx>>sy>>ex>>ey;
// cout<<mp[sx][sy]<<" "<<mp[ex][ey]<<endl;
if(mp[sx][sy]=='#'||mp[ex][ey]=='#')//判斷初始與結束位置
cout<<"NO"<<endl;
else
bfs();
}
}
本程式碼摘自https://www.cnblogs.com/sky-stars/p/11135249.html
總結:
由於BFS是將每一個可能的情況都列舉出來了,那麼第一次得到的一定是達到解的最短線路,在最短路問題中,很多演算法也是繼承於BFS的思想誕生的。但是由於BFS對於空間的佔用很大,相對的DFS對時間的需求也較高,多數題目要通過優化操作來實現這些演算法,才能通過。
本人蒟蒻一枚,也在不斷的學習中,若有錯誤歡迎批評指正,希望我的表達能夠引起更多的討論和思考! &n