DFS與BFS——理解簡單搜尋(中文虛擬碼+例題)

Simon5ei發表於2020-07-27

新的方法和概念,常常比解決問題本身更重要。————華羅庚

引子

深度優先搜尋(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,,As1s1 個數,表示第一科習題集每道題目所消耗的時間。 第 3 行,為 B1,B2,,Bs2s2 個數。 第 4 行,為 C1,C2,,Cs3s3 個數。 第 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對時間的需求也較高,多數題目要通過優化操作來實現這些演算法,才能通過。

本人蒟蒻一枚,也在不斷的學習中,若有錯誤歡迎批評指正,希望我的表達能夠引起更多的討論和思考!  

相關文章