深度優先搜尋演算法-dfs講解

To_string發表於2023-02-15

迷宮問題

有一個迷宮:

S**.
....
***T


(其中字元S表示起點,字元T表示終點,字元*表示牆壁,字元.表示平地。你需要從S出發走到T,每次只能向上下左右相鄰的位置移動,不能走出地圖,也不能穿過牆壁,每個點只能透過一次。)

現在需要你求出是否可以走出這個迷宮

我們將這個走迷宮過程稱為dfs(深度優先搜尋)演算法。

思路

當我們搜尋到了某一個點,有這樣3種情況:

1.當前我們所在的格子就是終點。

2.如果不是終點,我們列舉向上、向下、向左、向右四個方向,依次去判斷它旁邊的四個點是否可以作為下一步合法的目標點,如果可以,那麼我們就進行這一步,走到目標點,然後繼續進行操作。

3.當然也有可能我們走到了“死衚衕”裡(上方、下方、左方、右方四個點都不是合法的目標點),那麼我們就回退一步,然後從上一步所在的那個格子向其他未嘗試的方向繼續列舉。

怎樣才能算“合法的目標點”?

1.必須在所給定的迷宮範圍內

2.不能是迷宮邊界或牆。

3.這個點在搜尋過程中沒有被走過(這樣做是因為,如果一個點被允許多次訪問,那麼肯定會出現死迴圈的情況——在兩個點之間來回走。)

實現程式碼

#include <iostream>
using namespace std;
int n, m;
string maze[105];
int sx, sy;
bool vis[105][105];
int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};//四個方向的方向陣列
bool in(int x, int y) {
    return 0 <= x && x < n && 0 <= y && y < m;
}
bool dfs(int x, int y) {
    vis[x][y] = 1;//點已走過標記
    if (maze[x][y] == 'T') {//到達終點
        return 1;
    }
    for (int i = 0; i < 4; ++i) {
        int tx = x + dir[i][0];
        int ty = y + dir[i][1];
        if (in(tx, ty) && !vis[tx][ty] && maze[tx][ty] != '*') {
            /*
            1.in(tx, ty) : 即將要訪問的點在迷宮內
            2.!vis[tx][ty] : 點沒有走過
            3.maze[tx][ty] != '*' : 不是牆
            */
            if (dfs(tx, ty)) {
                return 1;
            }
        }
    }
    return 0;
}
int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        cin >> maze[i];
    }
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            if (maze[i][j] == 'S') {
                //記錄起點的座標
                sx = i;
                sy = j;
            }
        }
    }
    if (dfs(sx, sy)) {
        puts("Yes");
    } else {
        puts("No");
    }
    return 0;
}

深搜的剪枝最佳化

可行性剪枝

剪枝,顧名思義,就是透過一些判斷,砍掉搜尋樹上不必要的子樹。有時候,在搜尋過程中我們會發現某個結點對應的子樹的狀態都不是我們要的結果,那麼我們其實沒必要對這個分支進行搜尋,直接“砍掉”這棵子樹(直接 return退出),就是"剪枝"。

我們舉一個例子:

給定n個整數,要求選出K個數,使得選出來的K個數的和為sum。

 

 

 

如上圖,當k=2的時候,如果已經選了2個數,再往後選更多的數是沒有意義的。所以我們可以直接減去這個搜尋分支,對應上圖中的剪刀減去的那棵子樹。

又比如,如果所有的數都是正數,如果一旦發現當前和的值都已經大於sum了,那麼之後不管怎麼選,選擇數的和都不可能是sum了,就可以直接終止這個分支的搜尋。

例:從1,2,3,⋯,3030個數中選8個數,使得和為200

我們可以加如下剪枝

if (數字個數 > 8) return ;
if (總和 > 200) return ;

經過嘗逝後發現:

沒有剪枝

 

 加剪枝:

 

最優性剪枝

我們再看一個問題:

有一個n×m大小的迷宮。其中字元S表示起點,字元T表示終點,字元*表示牆壁,字元.表示平地。你需要從S出發走到T,每次只能向上下左右相鄰的位置移動,並且不能走出地圖,也不能走進牆壁。保證迷宮至少存在一種可行的路徑,輸出S走到T的最少步數。

對於求最優解(從起點到終點的最小步數)這種問題,通常可以用最優性剪枝,比如在求解迷宮最短路的時候,如果發現當前的步數已經超過了當前最優解,那從當前狀態開始的搜尋都是多餘的,因為這樣搜尋下去永遠都搜不到更優的解。透過這樣的剪枝,可以省去大量冗餘的計算。

此外,在搜尋是否有可行解的過程中,一旦找到了一組可行解,後面所有的搜尋都不必再進行了,這算是最優性剪枝的一個特例。

現在我們考慮用dfs來解決這個問題,第一個搜到的答案res並不一定是正解,但是正解一定小於等於res。於是如果當前步數大於等於res就直接剪枝

在dfs函式內加入如下程式碼

if (目前步數 >= res) return ;
if (目前所處的位置字元 == 'T') {
    答案 = 目前步數;//因為我們在剛才已經進行了一次剪枝,所以我們現在是可以保證目前答案大於之前答案的
    return ;
}

好啦,到這裡就結束了捏~

求贊qwq

相關文章