1. 程式人生 > 實用技巧 >簡單BFS和DFS的做題總結

簡單BFS和DFS的做題總結

僅僅是做過這些題,還不能說是掌握了搜尋,只是現階段就學到這裡了.之後的時間如果看到洛谷月賽裡面的搜尋可以做一做,進階更是之後的事情.

1.DFS

一條路走到黑,到頭了再重來,直至所有的情況被遍歷(或者剪枝).

入門題有全排列問題.可以看出來思想是在每一步都把之後所有可能的排列順序都遍歷一遍.每一個排列順序即對應於一個解.這裡把每一個解(即排列本身)直接輸出就行了.

用到類似做法的有吃乳酪問題,遍歷所有的順序,每一個順序對應於一個解,這裡解的求取方法是老鼠按這個遍歷順序吃掉所有乳酪所得到的移動距離之和.

注意,如何不重不漏地遍歷所有的順序呢?在上面兩個問題中,都用到了"used"這個陣列,並且都有類似於如下的結構:

    for(int i = 1; i <= n; i++){
        if(!used[i]){
            ...
            used[i] = true;
            dfs(...);
            used[i] = false;
        }
    }

先把狀態i標記為已在當前位置選擇,在此基礎上搜索下一個狀態便不會重複地選擇狀態i了.

在此後將狀態i重新標記為未使用會怎麼樣?當你進行下一次dfs時,狀態i可以再次被選用,但是這時候是在另一個位置上了,想象一下一個樹狀圖,狀態是如何在這個樹狀圖上面轉移的.

以全排列問題n=4的情況為例:

            3  → 4

         ↗

       2  →  4  → 3

    ↗       2 → 4

        ↗

1   →  3    

         ↘

    ↘       4 →  2

       4  → 2 →  3

         ↘

           3 →  2

(極簡藝術大師)

遍歷過程:1 2 3 4 ← ← 4 3 ← ← ← 3 2 4 ←← 4 2←←← 4 2 3←← 3 2←←← 結束

每一次←都表示返回到上一層的dfs.注意used在這個過程中的變化,很容易理解為什麼可以遍歷所有狀態.

這種做法叫做回溯,在dfs中是很常見的,很多情況還是必要的.

(演算法競賽入門-)經典的回溯題目有八皇后問題.裡面用到一種獨特的標記方法.

有時候,回溯可能不一定是簡單地把某狀態標記為true 或 false,比如這個例子.

自然數的拆分問題

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

int n, s[10];

void dfs(int add, int sum) {
    if (sum == n) {
        for (int i = 1; i <= n - 1; i++) {
            int tmp = s[i];
            while (tmp--) {
                printf("%d", i);
                if (tmp || s[i + 1] || s[i + 2] || s[i + 3] || s[i + 4] ||
                    s[i + 5] || s[i + 6] || s[i + 7] || s[i + 8])
                    printf("+");
            }
        }
        puts("");
    } else if (sum < n && add + sum <= n) {
        for (int i = add; i + sum <= n; i++) {
            s[i]++;
            dfs(i, sum + i);
            s[i]--;
        }
    }
}

int main() {
    cin >> n;
    if (n == 1)
        puts("1");
    else
        dfs(1, 0);

    return 0;
}
View Code

此外,map可以用來標記字串是否已經遍歷.例如:(雖然這題是bfs)

字串變換(注意map標記字串最好用string,用char*會帶來麻煩)

#include <algorithm>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
using namespace std;
struct S {
    string s;
    int t;
};

string A, B;
string Ai[30];
string Bi[30];
queue<S> que;
int n, ans;
map<string, int> used;

string canLink(const string &s, int i, int j) {
    string ans = "";
    if (i + Ai[j].length() > s.length()) return ans;

    for (int k = 0; k < Ai[j].length(); k++)
        if (s[i + k] != Ai[j][k]) return ans;

    ans = s.substr(0, i);
    ans += Bi[j];
    ans += s.substr(i + Ai[j].length());
    return ans;
}

int main() {
    cin >> A >> B;
    while (cin >> Ai[n] >> Bi[n]) n++;

    S s = {A, 0};
    que.push(s);

    while (!que.empty()) {
        S tp = que.front();
        que.pop();
        string tmp;

        if (used.count(tp.s) == 1) continue;

        if (tp.s == B) {
            ans = tp.t;
            break;
        }
        used[tp.s] = 1;
        for (int i = 0; i < tp.s.length(); i++)
            for (int j = 0; j < n; j++) {
                tmp = canLink(tp.s, i, j);
                if (tmp != "") que.push({tmp, tp.t + 1});
            }
    }

    if (ans > 10 || ans == 0)
        cout << "NO ANSWER!" << endl;
    else
        cout << ans << endl;
    return 0;
}
View Code

有時候,合理設定dfs(...)的引數可以省去回溯,使得過程更加簡潔.見此.這種情況並不常見.

除此之外還有做法比較簡單的剪枝.

如果dfs以上一狀態某個表示解的過程值為引數,那麼當發現過程值已經劣於先前發現的解,那麼就沒比較繼續搜尋下去了,直接return.直接放上兩個例子.

吃乳酪問題 奇怪的電梯

可以看到如下結構:

if(當前狀態 != 最終狀態 && ct <= ans){        // 只有當前過程值ct不大於已經發現的解才會繼續搜尋
        ...(搜尋操作)
    }else
        ans = min(ans, ct);        // 目標是找到最小的ct的值    

這就是我所見到的大部分dfs剪枝的方法,如果剪枝也不行的話,見吃乳酪問題裡的卡時.但這時候應該換演算法了.

如你所見,dfs的大部分題目都會同時用到剪枝和回溯.

2.BFS

有相當多的題目,DFS和BFS都可以做,只是效率有所區別.

一道近乎於模板的bfs:好奇怪的遊戲.

注意vis這個標記,在bfs中一個點第一次被遍歷到時一定是最快/最短的路徑,所以在vis標記之後就不需要再去這個點了.(隱性的剪枝)

如果用dfs+回溯來做的話,就必須等到所有的方案(剪枝後的)窮舉之後才能得出答案,相比bfs效率會低很多.

bfs每標記一個vis,都是在向正確答案靠近一步.

dfs回溯完畢時,才能夠"總結"出正確答案.

所以求到某已知狀態的最短路的問題適合用bfs.

如果最終狀態不給出的話,怎麼用bfs呢?比如說吃乳酪.這時候暴力方法還是用dfs.

這題的dfs做法轉載於此.

#include<bits/stdc++.h>//萬能頭
using namespace std;
int a,b,c,d,f[25][25],dx[13]={0,2,2,2,2,1,1,-1,-1,-2,-2,-2,-2},dy[13]={0,2,1,-1,-2,2,-2,2,-2,2,1,-1,-2};//前面已經說過了,不解釋(貌似已經解釋了QWQ)
void dfs(int x,int y,int s){//到達第x行y列,步數為x
    f[x][y]=s;//標記
    for(int u=1;u<=12;u++){//開始拓展
        int ux=x+dx[u],uy=y+dy[u];//找到座標
        if(ux<1||ux>20||uy<1||uy>20)//判邊界
            continue;
        if(f[ux][uy]==0||f[ux][uy]>s+1)//可以拓展
            dfs(ux,uy,s+1);//拓展
    }
}
int main()
{   scanf("%d%d%d%d",&a,&b,&c,&d);
    dfs(a,b,1);
    printf("%d\n",f[1][1]-1);
    memset(f,0,sizeof(f));//別忘清0
    dfs(c,d,1);
    printf("%d\n",f[1][1]-1);
    return 0;
}
View Code

注意到

if(f[ux][uy]==0||f[ux][uy]>s+1)

所產生的效果是於bfs等同的.實際上具有相同的思想,即記錄到達某點時的最短路徑,在此基礎上向各向拓展並最終到達終點.

區別在於bfs從起點向四周逐層拓展,dfs則一個方向走到底後回頭繼續走.至於這時效率的區分,我是懶得計算這裡的複雜度的.

也許只要bfs或者dfs可以求解的問題,另一者總是可以求解,只是在設計上面會有所困難?做題時常常會看到題解區同時存在兩種解法,但是第一眼看上去是想不到另一者是怎麼做的.

想要深入研究看這裡吧,暫時只需要做到能夠快速做出選擇並AC就行了,不需要過於糾結.

再放兩道bfs的題,就寫到這裡吧.

[USACO08FEB]Meteor Shower S

又做了一段時間的題目才發現這個標記方法還是非常常用的,幾乎算是bfs模板的一部分了,當時還是太菜了

[USACO11OPEN]Corn Maze S

標記還是不標記?這是個問題.