算法专题 | 深度优先搜索(DFS)

DFS(系统栈,for循环,递归)

重要概念

当然搜索DFS不一定要通过递归来实现,用循环也是可以的

一下子懂了DFS:dfs()函数里会有个循环,然后循环里有个if判断条件,满足条件的情况时一条条分支
在这里插入图片描述

注意:我下标一般是从1开始的,所以填数时也是从u = 1(第二层)开始填的,如g[u + 1][i]

  • 剪枝:能够判断出不是可行解或不是最优解
    一般进行剪枝后,就难以分析时间复杂度了
  • 出口:确定出口的方法:看当前已经搜到的个数,以及u的值
    如:排列数字中dfs中参数u = 1时已经搜到了1个数,所以 u = n u = n 时,说明已经搜到了n个数,所以可以设为出口;LeetCode79单词搜索中dfs参数 u = 1 u = 1 时已经搜到了2个数,所以你要搜n个数,那设置出口 u = n 1 u = n - 1 即可
  • 恢复现场:保证下一个分支的搜索能正常进行
    比如上图第一数填充了1后,继续搜索,有两个分支,如果第一个分支把2标记为true,并完成该分支搜索后,未把2重新标记为false,那么for循环进行到下一个分支时,2已经被提前舍弃了,这显然是不合理的,这让下一个分支本有的选择变少了。
    所以弄乱了别人的东西一定要整理好恢复原状才能离开

排列数字

题目:输出1到n的所有全排列

注意:我习惯path数组从下标1开始填数

// 2020年1月13日 星期一 10:29:57
#include <iostream>

using namespace std;

const int N = 10;

int n;
int path[N];
bool st[N]; // 用来判断搜下一层的位置 可以选什么

// 通过u判断层数 u = 0表示第1层
// DFS就是把所有情况搜一遍,也可以加个判断条件找最优(剪枝)

void dfs(int u) {
    if (u == n) {
        for (int i = 1; i <= n; i ++ ) printf("%d ", path[i]);
        puts("");
        return;
    }

    for (int i = 1; i <= n; i ++) // for循环有几个if条件成立,当前结点就有几个分支
        if (!st[i]) {   
            path[u + 1] = i;      // 一开始u = 0,u + 1表示从第2层开始填数,具体可以看图
            st[i] = true;         // true表示i用过了
            dfs(u + 1);           // 对u + 1位置进行填数,递归
            path[u + 1] = 0;      // 此题,这步可有可无,因为会被覆盖,但一般题目建议都加上!
            st[i] = false;        // 恢复现场(方便下一个分支能正常搜索)
        }
}

int main() {
    cin >> n;

    dfs(0); // 0表示从树根全是空位开始

    return 0;
}

n皇后问题

从左上角至右下角及其平行的均为对角线
对于dg[u + i - 1],udg[n - u + i],画个图就能确定表达式
在这里插入图片描述
按行枚举

// 2020年1月13日 星期一 11:06:23

// 按行搜索,所以不用row[N]
#include <iostream>

using namespace std;

const int N = 20; // 要比n的两倍大一些
char g[N][N];
bool col[N], dg[N], udg[N];
int n;

// bool数组用来判断搜索的下一个位置是否可行
// col列,dg对角线(左上到右下),udg反对角线
// g[N][N]用来存路径

void dfs(int u) {
    if (u == n) {
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= n ;j ++) {
                cout << g[i][j];
            }
            cout << endl;
        }
        cout << endl;
    }
    
    // 对n个位置按行搜索
    for (int i = 1; i <= n; i ++) {
        // 剪枝(对于不满足要求的点,不能继续往下搜索)  
        
        if (!col[i] && !dg[u + i - 1] && !udg[u - i + n]) {
            g[u + 1][i] = 'Q';
            col[i] = dg[u + i - 1] = udg[u - i + n] = true;
            dfs(u + 1);
            g[u + 1][i] = '.';                               // 恢复现场,这步必须有,因为这题不像排列数字那题都能被覆盖到
            col[i] = dg[u + i - 1] = udg[u - i + n] = false; // 恢复现场,不影响下一个分支的搜索,这步很关键
        }
    }
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n ;j ++) {
            g[i][j] = '.';
        }
    }
    
    dfs(0);
    
    return 0;
}


枚举每个位置

#include <iostream>

using namespace std;

const int N = 20;
int x, y;
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];

// s表示已经放了几个皇后
void dfs(int x, int y, int s) {
    if (y == n + 1) {
        y = 1, x ++;
    }
    if (x == n + 1) {
        if (s == n) {
            for (int i = 1; i <= n; i ++) {
                for (int j = 1; j <= n ;j ++) {
                    cout << g[i][j];
                }
                cout << endl;
            }
            cout << endl;
        }
        return;
    }
    
    // 相当于按行枚举中,for(int i = 0; i < 2; i++) if (i == 0) ... if (i == 1) ..
    // 当前位置放皇后
    if (!row[x] && !col[y] && !dg[x + y - 1] && !udg[y + n - x]) {
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x + y - 1] = udg[y + n - x] = true;
        dfs(x, y + 1, s + 1);
        g[x][y] = '.';
        row[x] = col[y] = dg[x + y - 1] = udg[y + n - x] = false;
    }
    
    // 当前位置不放皇后
    dfs(x, y + 1, s); 
    
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            g[i][j] = '.';
        }
    }
   
    dfs(1, 1, 0);
    
    return 0;
}

LeetCode51.N皇后

在这里插入图片描述

class Solution {
public:
    static const int N = 30;
    char g[N][N];
    bool col[N], dg[N], udg[N];
   
    vector<vector<string>> ans;

    void dfs(int u, int n) {
        if (u == n) {
            vector<string> sol;
            for(int i = 1; i <= n; i ++) {
                string tmp = "";
                for (int j = 1; j <= n; j ++) {
                    tmp += g[i][j];
                }
                sol.push_back(tmp);
            }
            ans.push_back(sol);
            return;
        }

        for (int i = 1; i <= n; i ++) {
            if (!col[i] && !dg[u + i - 1] && !udg[u - i + n]) {
                g[u + 1][i] = 'Q';                               // 从u = 1开始填
                col[i] = dg[u + i - 1] = udg[u - i + n] = true;
                dfs(u + 1, n);
                g[u + 1][i] = '.';                               // 恢复现场,这步必须有,因为这题不像排列数字那题都能被覆盖到
                col[i] = dg[u + i - 1] = udg[u - i + n] = false; // 恢复现场,不影响下一个分支的搜索,这步很关键
            }
        }
    }

    vector<vector<string>> solveNQueens(int n) {
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= n ; j ++) {
                g[i][j] = '.';
            }
        }
        dfs(0, n);
        return ans;
    }
};

46.全排列

在这里插入图片描述

class Solution {
public:
    static const int N = 30;
    bool st[N];
    vector<int> path;
    vector<vector<int>> ans;
    
    void dfs(int u, int n, vector<int>& nums) {
        if (u == n) {
            ans.push_back(path);
            return;
        }
        
        for (int i = 1; i <= n; i ++) {
            if (!st[i]) {
                path.push_back(nums[i - 1]);
                st[i] = true;
                dfs(u + 1, n, nums);
                path.pop_back(); // 恢复现场
                st[i] = false;
            }
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        int n = nums.size();
        dfs(0, n, nums);
        return ans;
    }
};

17. 电话号码的字母组合(DFS)

class Solution {
public:
    vector<string> res;
    string str;

    vector<string> table = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    void dfs(int u, string digits) {
        int n = digits.size();
        if (u == n) {
            res.push_back(str); // str = "ad"
            return;
        }
        // 如digits[u] = '3',则table对于的下标为1
        for (int i = 0; i < table[digits[u] - '2'].size(); i ++) { // 分支的所有可能
            // 此题所有分支都是可行的
            str.push_back(table[digits[u] - '2'][i]); // string也可以用push_back(x);
            dfs(u + 1, digits);
            str.pop_back();
        }

    }

    vector<string> letterCombinations(string digits) {
        if (digits == "") return res;
        // digits = "23"
        dfs(0, digits);
        return res;
    }
};

79.单词搜索(DFS好题)

在这里插入图片描述

分析:遍历矩阵中的每个点,对每个点进行dfs搜索,如果有一个搜索满足条件,则结果为true

版本1 习惯
注意st[][]的位置

class Solution {
public:
    static const int N = 1000;
    int n, m;
    int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
    bool st[N][N];

    bool exist(vector<vector<char>>& board, string word) {
        if(board.empty() || board[0].empty()) return false;

        n = board.size(), m = board[0].size();
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < m; j ++) {
                st[i][j] = true; // 把起点设为访问过的点
                if(board[i][j] == word[0] && dfs(board, i, j, word, 0)) return true;
                st[i][j] = false;
            }
        }
        return false;
    }

    bool dfs(vector<vector<char>> & board, int sx, int sy, string & word, int u) {
        if (u == word.size() - 1) return true;
    
        for (int i = 0; i < 4; i ++) {
            int a = sx + dx[i], b = sy + dy[i];
            // 3个if用来剪枝,类似n皇后的if条件
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (st[a][b] == true) continue;
            if (board[a][b] != word[u + 1]) continue;

            st[a][b] = true;
            if (dfs(board, a, b, word, u + 1)) return true; // 只要有一个是true就会一直返回true
            st[a][b] = false;  // 恢复现场,不影响其他分支
        }
       
        return false;
    }

};

版本2

class Solution {
public:
    static const int N = 1000;
    int n, m;
    int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
    bool st[N][N];

    bool exist(vector<vector<char>>& board, string word) {
        if(board.empty() || board[0].empty()) return false;

        n = board.size(), m = board[0].size();
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < m; j ++) {
                if(dfs(board, i, j, word, 0)) return true;
            }
        }
        return false;
    }

    bool dfs(vector<vector<char>> & board, int sx, int sy, string & word, int u) {
        if (board[sx][sy] != word[u]) return false;
        if (u == word.size() - 1) return true;

        st[sx][sy] = true;
        for (int i = 0; i < 4; i ++) {
            int a = sx + dx[i], b = sy + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (st[a][b] == true) continue;
            if (dfs(board, a, b, word, u + 1)) return true;
        }
        st[sx][sy] = false; // 恢复现场,不影响其他分支
        return false;
    }

};

Acwing 1116.马走日

在这里插入图片描述
分析:DFS遍历8个方向,注意恢复现场以及如何确定出口u的取值

#include <iostream>

using namespace std;

int n, m, x, y;
int res = 0;
bool st[11][11];

int dx[] = {1,  1,  2, 2, -1, -1, -2, -2};
int dy[] = {2, -2, -1, 1,  2, -2,  1, -1};


void dfs(int sx, int sy, int u) {
    if (u == n * m - 1) {
        res ++;
        return;
    }
    
    for (int i = 0; i < 8; i ++) {
        int a = sx + dx[i], b = sy + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= m) continue;
        if (st[a][b] == true) continue; // 已访问过
        st[a][b] = true;
        dfs(a, b, u + 1);
        st[a][b] = false;
    }
}

int main() {
    std::ios::sync_with_stdio(false); // 不加这个优化,最后一个用例就会超时
    int T;
    cin >> T;
    while(T --) {
        cin >> n >> m;
        cin >> x >> y;
        
        res = 0;
        st[x][y] = true;
        dfs(x, y, 0);
        cout << res << endl;
        st[x][y] = false; // 恢复现场
    }
    
    
    return 0;
}
发布了182 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_43827595/article/details/103968010