LeetCode题解之DFS(一)

172 阅读5分钟

1.DFS基础知识

题目中常用到的深度优先搜索范围:
1.矩阵中从当前节点往上下左右等方向搜索节点...
2.树状结构中沿着左右子节点搜索...
3.其余类型因题而异...
最后呈现的结果都是从一个切入点发散出来的dfs+回溯(全局恢复现场) ,有的题目不用恢复现场;dfs调用函数也是递归,可是一般来说没有返回值,每个节点执行到子节点最右侧叶子就表示该点的dfs完全结束了,

注意:对于复杂度有要求的题目需要有效的剪枝操作!

2.全排列题目

46. 全排列

class Solution {
public:
/*
1.切入点:
    选择范围:每个位置可以填[0,n-1]个数
    选择条件:该数没有被用过
    剪枝条件:找到一种答案
    恢复现场:path.pop_back()和状态st,便于这个数字在下一层使用
2.关键的全局变量:
    每个排列的path;(因为在循环中做的选择,所以path是在变化的
    1-n个数字是否使用过的bool状态数组st;
*/
    vector<bool> st;
    vector<int> path;
    vector<vector<int>> res;
    int n;
    vector<vector<int>> permute(vector<int>& nums) {
        n = nums.size();
        st = vector<bool> (n, 0); 
        dfs(nums, 0);
        return res;
    }
    void dfs(vector<int>& nums, int u)
    {
        if(u == n) {res.push_back(path); return;}
        for(int i = 0; i < n; i ++)//选择范围
        {
            if(!st[i])//选择条件
            {
                st[i] = true;
                path.push_back(nums[i]);
                dfs(nums, u + 1);//递归
                st[i] = false;//恢复现场
                path.pop_back();
            }
        }
        return;
    }
};

内部剪枝:剑指 Offer 38. 字符串的排列(47. 全排列 II)

例如'122',以前的全排列会出现同一个位置取两次2的情况,可想而知这两种情况的子数都是一模一样的,也就是重复。所以只用对出现的第一个2进行dfs,先排序,让相同元素挨在一起,这样在同层的时候遍历到第二个2,发现和前面一个数相等,就可以剪枝了,前一个数st=0表示这两个2在同层,否则不一定是同层,不同层是不能直接剪枝的,

与上一题相比就多了剪枝的三个条件而已~

class Solution {
public:
/*
1.切入点:
    选择范围:每个位置可以填[0,n-1]个数
    选择条件:该数没有被用过
    剪枝条件:(1)找到一种答案(2)内部剪枝:排序后的选择范围中前一个相同数已经用过了(st清0),这个数再用就会重复了
    恢复现场:path.pop_back()和状态st,便于这个数字在下一层使用
2.关键的全局变量:
    每个排列的path;(因为在循环中做的选择,所以path是在变化的
    1-n个数字是否使用过的bool状态数组st;
*/
    map<int,bool> st;
    vector<string> res;
    string tmp;
    int n;
    vector<string> permutation(string s) {
        n = s.size();
        if(!n) return {};
        vector<char> news;
        for(int i = 0; s[i]; i++) news.push_back(s[i]);
        sort(news.begin(),news.end());
        dfs(news, 0);
        return res;
    }
    void dfs(vector<char> news,int u)
    {
        if (u == n) {res.push_back(tmp); return; }
        for(int i = 0;i < n;i++)//选择范围
        {
            if(!st[i])//范围的条件+剪枝
            {
                //因为用到i-1所以需要i>0,同层需要st[i-1]=0
                if(i>0 && news[i] == news[i-1] && !st[i-1]) continue;//剪枝,三个条件
                tmp += news[i];//做选择
                st[i] = true;//设置当前数已用
                dfs(news, u+1); //进入下一层
                st[i] = false;//撤销选择
                tmp.pop_back();//撤销选择
                
            }
        }
        return;
    }
};

39. 组合总和

class Solution {
public:
/*
1.切入点:
    选择范围:每个位置可以填[2,3,6,7]几个数,
    选择条件:无
    剪枝条件:这个答案不不行or找到一种答案
    恢复现场:path.pop_back()
2.关键的全局变量:
    每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
    vector<vector<int>> ans;
    vector<int> tmp;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
       dfs(candidates,target,0);
       return ans; 
    }
    void dfs(vector<int>& candidates,int target,int u)
    {
        if(target == 0)//返回条件
        {
            ans.push_back(tmp);
            return;
        }
        if(target < 0) return;//返回条件
        for(int i = u; i < candidates.size(); i ++)//选择范围
        {
            tmp.push_back(candidates[i]);
            dfs(candidates, target - candidates[i], i);//递归
            tmp.pop_back();//恢复现场
        }
    }
};

216. 组合总和 III

class Solution {
public:
/*
1.切入点:
    选择范围:每个位置可以填[u,9]几个数,
    选择条件:无
    剪枝条件:这个答案不不行or找到一种答案
    恢复现场:path.pop_back()
2.关键的全局变量:
    每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
    class Solution {
public:
/*
1.切入点:
    选择范围:每个位置可以填[u,9]几个数,
    选择条件:无
    剪枝条件:这个答案不不行or找到一种答案
    恢复现场:path.pop_back()
2.关键的全局变量:
    每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
    vector<vector<int>> ans;
    vector<int> tmp;
    vector<vector<int>> combinationSum3(int k, int n) {
       dfs(k, 1, n);
       return ans;
    }
    void dfs(int k,int u,int n)//把需要的信息写到参数里即可
    {
        if(k == 0)//返回条件
        {
            if(n == 0) ans.push_back(tmp);
            else return;
        }
        for(int i = u; i <=9; i ++)//选择范围
        {
            tmp.push_back(i);
            dfs(k - 1, i + 1, n - i);//递归
            tmp.pop_back();//恢复现场
        }
    }
};

22. 括号生成

class Solution {
public:
/*
1.切入点:
    选择范围:'(',')'两种括号
    选择条件:无,直接遍历
    剪枝条件:个数超标or找到一种答案
2.关键的全局变量:
    本题不用恢复现场,因为对每一层的两个dfs参数path都是一样的,不像以前在遍历过程中做选择!所以不用pop_back
*/
    vector<string> ans;
    int n;
    vector<string> generateParenthesis(int _n) {
        n = _n;
        dfs("", 0, 0);
        return ans;
    }
    void dfs( string path, int l,int r)
    {
        if(r > l || l > n || r > n) return;//返回条件
        if(l == r && l == n)//返回条件
        {
            ans.push_back(path);
            return;
        }
        dfs(path + '(' , l + 1, r );//选择范围
        dfs(path + ')', l ,r + 1);
        
    }
    
};

3.矩阵中的DFS

52. N皇后 II

class Solution {
public:
/*
1.切入点:
    选择范围:每一行可以选择[0,n-1]个位置
    选择条件:col d ud不攻击
    剪枝条件:找到一种答案
    恢复现场:修改col d ud状态
2.关键的全局变量:
    col d ud的状态数组
*/
    vector<bool> col, d, ud;
    int n;
    int ans;
    int totalNQueens(int _n) {
        n = _n;
        col = vector<bool> (n, 0);
        d = ud = vector<bool> (2 * n, 0);
        dfs(0);
        return ans;
    }
    void dfs(int u)
    {
        if(u == n)//返回条件
        {
            ans ++;
            return;
        }
        for(int i = 0; i < n; i ++)//选择范围
        {
            if(!col[i] && !d[i + u] && !ud[i - u + n])//选择条件
            {
                col[i] = d[u + i] = ud[i - u + n] = true;
                dfs(u + 1);//递归
                col[i] = d[u + i] = ud[i - u + n] = false;//恢复现场
            }
        }
        return;
    }
};

79. 单词搜索(剑指 Offer 12. 矩阵中的路径)

class Solution {
public:
/*
1.切入点:
    选择范围:每个点可以往4个方向搜索
    选择条件:坐标满足
    剪枝条件:值不相等or找到一种答案
    恢复现场:修改该点状态st
2.关键的全局变量:
    直接在矩阵中修改字母为'.',防止重复
*/
    int n,m;
    int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};//上右下左
    bool exist(vector<vector<char>>& board, string word) {
        //时间复杂度o(nm3^k)
        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;//所有点都没搜到才是false
    }
    bool dfs(vector<vector<char>>& board,int x,int y ,string word,int u)
    {
        if(board[x][y] != word[u]) return false;//返回条件
        if(u == word.size()-1) return true;//返回条件
        //如果等于,就从这个点开始dfs
        board[x][y] = '.';
        for(int i = 0;i < 4; i ++)//选择范围
        {
            int a = x + dx[i],b = y + dy[i];
            if(a >= 0 && a < n && b >= 0 && b < m)//选择条件
                if (dfs(board, a, b, word, u + 1)) return true;//有一个方向可以
        }
        board[x][y] = word[u];//恢复现场
        return false;//4个方向都不行的话
    }

};

200. 岛屿数量

class Solution {
public:
/*
1.切入点:
    选择范围:每个点可以往4个方向搜索
    选择条件:坐标满足&&没被遍历过&&是岛屿
    剪枝条件:无
    恢复现场:
2.关键的全局变量:
    本题为故意不恢复现场的一种应用
*/
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1 , 0, -1};
    int cnt = 0;
    int n, m;
    vector<vector<char>> grid;
    int numIslands(vector<vector<char>>& _grid) {
        grid = _grid;
        n = grid.size();
        m = grid[0].size();
        for(int i = 0; i < n; i ++)
            for(int j = 0; j < m; j ++)
                if(grid[i][j] == '1') 
                {
                    cnt ++;
                    dfs(i, j);
                }
        return cnt;
    }
    void dfs(int x, int y)//dfs把xy连着的点全部变成0
    {
        grid[x][y] = '0';
        for(int i = 0; i < 4; i ++)
        {
            int a = dx[i] + x;
            int b = dy[i] + y;
            if(a >= 0 && a < n && b >= 0 && b < m && grid[a][b] == '1')
            {
                grid[a][b] = '0';
                dfs(a, b);
            }
        }
    }
};

4.挖坑

90. 子集 II

class Solution {
public:
/*
1.切入点:
    选择范围:
    选择条件:同层不与前一个元素重复
    剪枝条件:到了最后一个dfs就停止
    恢复现场:path.pop_back();
2.关键的全局变量:
    path
*/
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        dfs(nums,0);
        return res;
    }
    void dfs(vector<int>& nums, int u)
    {
        res.push_back(path);//全排列是只在u==n时加入,子集是在dfs过程中加入
        //if(u == nums.size()) return ;//返回条件
        for(int i = u; i < nums.size(); i ++)//选择范围
        {
            if(i > u && nums[i] == nums[i - 1]) continue;//剪枝
            path.push_back(nums[i]);
            dfs(nums, i + 1);//递归
            path.pop_back();//恢复现场
        }
    }
};

37. 解数独

class Solution {
public:
    bool row[9][9] = {0}, col[9][9] = {0}, cell[3][3][9] = {0};
    void solveSudoku(vector<vector<char>>& board) {
        //遍历所有点,将初始化的数字状态记录下来
        for(int i = 0; i < 9; i ++)
            for(int j = 0; j < 9; j ++)
            {
                char c = board[i][j];
                if(c != '.')
                {
                    int t = c-'1';
                    row[i][t] = col[j][t] = cell[i/3][j/3][t] = true;//标记已经出现了的点
                }
            }
        dfs(board,0,0);//dfs行列
    }
    bool dfs(vector<vector<char>>& board, int x, int y)
    {
        if(y == 9) x ++, y = 0;//某一行已经填完了,跳到某一行起始位置
        if(x == 9) return true;//返回条件
        if(board[x][y] !='.') return dfs(board, x, y + 1);//返回条件
        for(int i = 0; i < 9; i ++)//9个数字,选择范围
            if(!row[x][i] && !col[y][i] && !cell[x/3][y/3][i])//选择条件
            {
                board[x][y] = '1' + i;
                row[x][i] = col[y][i] = cell[x/3][y/3][i] = true;
                if(dfs(board, x, y + 1)) return true;//找到一个就可以返回,最后的false是该点一个都不对的情况
                row[x][i] =col[y][i]=cell[x/3][y/3][i] = false;//恢复现场
                board[x][y] = '.';
            }
        return false;
    }
};

剑指 Offer 13. 机器人的运动范围

class Solution {
public:
    int get_single(int x)//算一个数的坐标位数和
    {
        int s = 0;
        while(x) s += x % 10, x /= 10;
        return s;
    }
    int get_sum(pair<int,int> p)//算横纵的
    {
        return get_single(p.first)+ get_single(p.second);
    }
    int res;
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    int n, m;
    vector<vector<bool>> st;
    int movingCount(int rows, int cols, int k) {
        n = rows;
        m = cols;
        st = vector<vector<bool>> (n, vector<bool> (m, false));
        res ++;
        st[0][0] = true;
        dfs(0, 0, k);
        return res;
    }
    void dfs(int x, int y, int k)//遍历到每个点就好,对每个点判断
    {
        for(int i = 0; i < 4; i ++)
        {
            int a = dx[i] + x, b = dy[i] + y;
            if(a < n && a >=0 && b < m && b >= 0 && get_sum({a,b}) <= k && !st[a][b])
            {
                res ++;
                st[a][b] = true;
                dfs(a, b, k);
            }
        }  
    }
};

3. 挖坑

473. 火柴拼正方形

class Solution {
public:
    vector<bool> st;
    bool makesquare(vector<int>& nums) {
        //能否凑出4个相同数
        //经典的剪枝问题
        //优先枚举数字大的,这样分支少,会有更多的枝被剪掉
        int sum = 0;
        for(auto u : nums) sum += u;
        if(!sum || sum % 4) return false;//sum为0或者不能被4整除
        //1.排序,从大到小枚举
        sort(nums.begin(), nums.end());
        reverse(nums.begin(), nums.end());//从大到小排列
        st = vector<bool> (nums.size());
        return dfs(nums, 0, 0, sum / 4);//第几条边、当前长度、应有的长度
    }
    bool dfs(vector<int>& nums,int u,int cur, int length)
    {
        if(cur == length) u ++, cur = 0;//当前长度==应有的长度
        if(u == 4) return true;
        for(int i = 0; i < nums.size(); i ++)//每边内部从大到小
            if(!st[i] && cur + nums[i] <= length)
            {
                st[i] = true;
                if(dfs(nums, u, cur + nums[i], length)) return true; 
                st[i] = false;
                if(!cur) return false;//2.是当前边的第一条,不满足时剪枝
                if(cur + nums[i] == length) return false;//3.当前边的最后一条,不满足剪枝
                while(i + 1 < nums.size() && nums[i + 1] == nums[i]) i ++;//4.跳过长度相同的木棍
            }
        return false;
    }
};