DFS查找专题

1.1 Word Search 给定一个二维平板和一个单词,请找出这个单词是否在二维平板中出现。

单词可以由平板中的邻接单元组成,这里的“邻接”定义为上下左右四个方向。
同一个单元上的字母最多只能使用一次。
DFS查找专题_第1张图片
在深度优先搜索中,最重要的就是考虑好搜索顺序。
我们先枚举单词的起点,然后依次枚举单词的每个字母。
过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。

时间复杂度分析:单词起点一共有 n2n2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O(n2*3k)

class Solution {
public:
    bool exist(vector<vector<char>>& board, string str) {
        for (int i = 0; i < board.size(); i ++ )
            for (int j = 0; j < board[i].size(); j ++ )
                if (dfs(board, str, 0, i, j))
                    return true;
        return false;
    }

    bool dfs(vector<vector<char>> &board, string &str, int u, int x, int y) {
    	//不匹配直接返回false,进入下一步循环,匹配则继续执行
        if (board[x][y] != str[u]) return false;
        //字符串全部匹配成果则返回true
        if (u == str.size() - 1) return true;
        int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
        char t = board[x][y];
        board[x][y] = '*';
        for (int i = 0; i < 4; i ++ ) {//每个字母都在四个方向去遍历
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < board.size() && b >= 0 && b < board[a].size()) {
            	//不匹配则回溯回该字母的下一个方向
                if (dfs(board, str, u + 1, a, b)) return true;
            }
        }
        board[x][y] = t;//四个方向都不匹配,则回溯回上一个字母的下一个方向,并恢复现场
        return false;
    }
};

1.2 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
DFS查找专题_第2张图片
(递归) O(4^l)

  • 可以通过手工或者循环的方式预处理每个数字可以代表哪些字母。
  • 通过递归尝试拼接一个新字母。 递归到目标长度,将当前字母串加入到答案中。
  • 注意,有可能数字串是空串,需要特判。
class Solution {
public:
    vector<char> digit[10];
    vector<string> res;

    void init() {
        char cur = 'a';
        for (int i = 2; i < 10; i++) {
            for (int j = 0; j < 3; j++)
                digit[i].push_back(cur++);
            if (i == 7 || i == 9)
                digit[i].push_back(cur++);
        }
    }

    void solve(string digits, int d, string cur) {
        if (d == digits.length()) {
            res.push_back(cur);
            return;
        }

        int cur_num = digits[d] - '0';

        for (int i = 0; i < digit[cur_num].size(); i++)
            solve(digits, d + 1, cur + digit[cur_num][i]);
    }

    vector<string> letterCombinations(string digits) {
        if (digits == "")
            return res;
        init();
        solve(digits, 0, "");
        return res;
    }
};

1.3 给定一个字符串,只包含数字。请解码出所有合法的IP地址。
DFS查找专题_第3张图片

(暴力搜索) O(C3n−1)
直接暴力搜索出所有合法方案。
合法的IP地址由四个0到255的整数组成。我们直接枚举四个整数的位数,然后判断每个数的范围是否在0到255。

时间复杂度分析:一共 n个数字,n−1个数字间隔,相当于从 n−1个数字间隔中挑3个断点,所以计算量是 O(C3n−1)

class Solution {
public:
    vector<string> ans;
    vector<int> path;

    vector<string> restoreIpAddresses(string s) {
        dfs(0, 0, s);
        return ans;
    }

    // u表示枚举到的字符串下标,k表示当前截断的IP个数,s表示原字符串
    void dfs(int u, int k, string &s)
    {
        if (u == s.size())
        {
            if (k == 4)
            {
                string ip = to_string(path[0]);
                for (int i = 1; i < 4; i ++ )
                    ip += '.' + to_string(path[i]);
                ans.push_back(ip);
            }
            return;
        }
        if (k > 4) return;

        unsigned t = 0;
        for (int i = u; i < s.size(); i ++ )
        {
            t = t * 10 + s[i] - '0';
            if (t >= 0 && t < 256)
            {
                path.push_back(t);
                dfs(i + 1, k + 1, s);
                path.pop_back();
            }
            if (!t) break;
        }
    }
};
2 排列

2.1 给出一列互不相同的整数,返回其全排列。
DFS查找专题_第4张图片
(回溯) O(n×n!)
我们从前往后,一位一位枚举,每次选择一个没有被使用过的数。
选好之后,将该数的状态改成“已被使用”,同时将该数记录在相应位置上,然后递归。
递归返回时,不要忘记将该数的状态改成“未被使用”,并将该数从相应位置上删除。

时间复杂度分析:

搜索树中最后一层共 n! 个叶节点,在叶节点处记录方案的计算量是 O(n),所以叶节点处的计算量是 O(n×n!)
搜索树一共有 n!+n!2!+n!3!+…=n!(1+12!+13!+…)≤n!(1+12+14+18+…)=2n! 个内部节点,在每个内部节点内均会for循环 n次,因此内部节点的计算量也是 O(n×n!)。 所以总时间复杂度是 O(n×n!)

class Solution {
public:
    vector<vector<int>> res;
    vector<bool> st;
    vector<int> path;
    
    vector<vector<int> > permute(vector<int> &num) {
        for(int i=0;i<num.size();i++)
            st.push_back(false);
        dfs(num,0);
        return res;
    }
    
    void dfs(vector<int> &num,int u){
        if(u==num.size()){
            res.push_back(path);
            return ;
        }
        
        for(int i=0;i<num.size();i++){
            if(!st[i]){
                st[i]=true;
                path.push_back(num[i]);
                dfs(num,u+1);
                st[i]=false;
                path.pop_back();
            }
        }
    }
};

2.2 给出一组可能包含重复项的数字,返回该组数字的所有排列

(回溯) O(n!)
由于有重复元素的存在,这道题的枚举顺序和 Permutations 不同。

  • 先将所有数从小到大排序,这样相同的数会排在一起;
  • 从左到右依次枚举每个数,每次将它放在一个空位上;
  • 对于相同数,我们人为定序,就可以避免重复计算:我们在dfs时记录一个额外的状态,记录上一个相同数存放的位置start,我们在枚举当前数时,只枚举 start+1,start+2,…,n这些位置。

不要忘记递归前和回溯时,对状态进行更新。
时间复杂度分析: 搜索树中最后一层共 n!个节点,前面所有层加一块的节点数量相比于最后一层节点数是无穷小量,可以忽略。且最后一层节点记录方案的计算量是 O(n),所以总时间复杂度是 O(n×n!)。

class Solution {
public:
    vector<bool> st;
    vector<int> path;
    vector<vector<int>> ans;

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        st = vector<bool>(nums.size(), false);
        path = vector<int>(nums.size());
        dfs(nums, 0, 0);
        return ans;
    }

    void dfs(vector<int>& nums, int u, int start)
    {
        if (u == nums.size())
        {
            ans.push_back(path);
            return;
        }

        for (int i = start; i < nums.size(); i ++ )
            if (!st[i])
            {
                st[i] = true;
                path[i] = nums[u];
                if (u + 1 < nums.size() && nums[u + 1] != nums[u])//排序后相同的数在一起
                    dfs(nums, u + 1, 0);
                else
                    dfs(nums, u + 1, i + 1);
                st[i] = false;
            }
    }
};

如果需要按照字典序排列,则采用方法二

/*枚举每个位置上放哪个数
以[1,1,2]为例:
                                     [ , , ]
                        /               |
               /                        |
           [1, , ]                   [2 ,, ]
        /           \                   |
    /                   \               |
[1,1, ]               [1,2, ]        [2,1, ]
   |                     |              |
[1,1,2]               [1,2,1]        [2,1,1]

*/

class Solution {
public:
    vector<bool> st;
    vector<vector<int>> res;
    vector<int> path;
    
    vector<vector<int> > permuteUnique(vector<int> &num) {
        sort(num.begin(),num.end());
        int n=num.size();
        st=vector<bool> (n,false);
        path=vector<int> (n);
        if(n==0)return res;
        dfs(num,0);
        return res;
    }
    
    void dfs(vector<int> &num,int u){
        
        if(u==num.size()){
            res.push_back(path);
            return ;
        }
        
        for(int i=0;i<num.size();i++){
            if(i>0&&st[i-1]&&num[i-1]==num[i])
                continue;
            if(!st[i]){
                st[i]=true;
                path[u]=num[i];
                dfs(num,u+1);
                st[i]=false;
            }
        }
    }
};

2.3 排列序列
DFS查找专题_第5张图片
(计数) O(n2)
做法:
从高位到低位依次考虑每一位;
对于每一位,从小到大依次枚举未使用过的数,确定当前位是几;
为了便于理解,我们这里给出一个例子的具体操作:n=4,k=14。
首先我们将所有排列按首位分组:
1 + (2, 3, 4的全排列)
2 + (1, 3, 4的全排列)
3 + (1, 2, 4的全排列)
4 + (2, 3, 4的全排列)
接下来我们确定第 k=14个排列在哪一组中。每组的排列个数是 3!=6个,所以第14个排列在第3组中,所以首位已经可以确定,是3。

然后我们再将第3组的排列继续分组:
31 + (2, 4的全排列)
32 + (1, 4的全排列)
34 + (1, 2的全排列)
接下来我们判断第 k=14 个排列在哪个小组中。我们先求第 14个排列在第三组中排第几,由于前两组每组有6个排列,所以第14个排列在第3组排第 14−6∗2=2。
在第三组中每个小组的排列个数是 2!=2个,所以第 k个排列在第1个小组,所以可以确定它的第二位数字是1。
依次类推,可以推出第14个排列是 3142。
时间复杂度分析:两重循环,所以时间复杂度是 O(n2)。

class Solution {
public:

    string getPermutation(int n, int k) {
        string res;
        vector<bool> st(n, false);
        for (int i = 0; i < n; i ++ ) //从高位到低位依次枚举每一位
        {
            int f = 1;
            for (int j = 1; j <= n - i - 1; j ++ ) f *= j; //计算 (n-i-1)!
            int next = 0;
            if (k > f) //确定当前位是第几个未使用过的数
            {
                int t = k / f;
                k %= f;
                if (k == 0) k = f, t -- ;
                while (t)
                {
                    if (!st[next]) t -- ;//没用过,则算上一次排列,t-1
                    next ++ ;
                }
            }
            while (st[next]) next ++ ;
            res += to_string(next + 1);
            st[next] = true;
        }

        return res;
    }
};

2.4 给定一个字符串S,我们可以将其中的大写字母换成小写字母,或将小写字母换成大写字母,从而得到一个新的字符串。
DFS查找专题_第6张图片
(DFS) O(n×2^n)
深度优先搜索。从左到右一位一位枚举:

  • 如果遇到数字,则直接跳过当前位,枚举下一位;
  • 如果遇到字母,则分别将当前位设成小写字母和大写字母,然后递归到下一位;

小技巧:可以用位运算改变当前字母的大小写,从而简化代码:将一个字母异或32,即可改变这个字母的大小写。比如:
‘a’ ^ 32 = ‘A’;
‘B’ ^ 32 = ‘b’;
时间复杂度分析:最坏情况下,所有字符都是字母,则每个字符都有两种选择,一共会得到 2^n 个字符串,最后将每个字符串记录在答案中还需要 O(n)O(n) 的计算量,所以总时间复杂度是 O(n×2n)。

class Solution {
public:

    vector<string> ans;

    vector<string> letterCasePermutation(string S) {
        dfs(S, 0);
        return ans;
    }

    void dfs(string &S, int u)
    {
        if (u == S.size())
        {
            ans.push_back(S);
            return;
        }
        dfs(S, u + 1);//不进行大小写变换
        if (S[u] >= 'A')//当前位不是数字时候进行大小写变换
        {
            S[u] ^= 32;
            dfs(S, u + 1);
        }
    }
};
3 组合

3.1 给出两个整数n和k,返回从1到n中取k个数字的所有可能的组合
(DFS) O(Ckn)
深度优先搜索,每层枚举第 u个数选哪个,一共枚举 k 层。由于这道题要求组合数,不考虑数的顺序,所以我们需要再记录一个值 start,表示当前数需要从几开始选,来保证所选的数递增。
时间复杂度分析:一共有 Ckn 个方案,所以时间复杂度是 O(Ckn)。

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    vector<vector<int> > combine(int n, int k) {
        dfs(0,1,n,k);
        return res;
    }
    
    void dfs(int u,int start,int n,int k){
        if(u==k){
            res.push_back(path);
            return;
        }
        
        for(int i=start;i<=n;i++){
            path.push_back(i);
            dfs(u+1,i+1,n,k);
            path.pop_back();
        }
    }
};

3.2 给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。
说明
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
DFS查找专题_第7张图片
(递归枚举)

  • 搜索的终止条件是层数超过的数组的长度或者当前数字组合等于目标值。
  • 剪枝:可以先将数组从小到大排序,搜索中如果 sum != target 并且 sum+candidates[i] > target,则可以直接终止之后的递归,因为之后的数字都会比 candidates[i] 大,不会再产生答案。
class Solution {
public:
    vector<vector<int> > combinationSum(vector<int> &candidates, int target) {
        vector<int> path;
        vector<vector<int>> res;
        sort(candidates.begin(),candidates.end());
        dfs(candidates,target,0,path,res);
        return res;
    }
    
    void dfs(vector<int> &num, int target,int start,vector<int> &path,vector<vector<int>> &res){
        if(target<0)return ;
        if(target==0){
            res.push_back(path);
            return;
        }
        
        for(int i=start;i<num.size();i++){//每个坑位都是num里的数据
            if (target < num[i]) {//再加上当前数超过目标值则退出
                return ;
            }
            path.push_back(num[i]);
            dfs(num,target-num[i],i,path,res);//下一个坑位从当前的第i个数字开始遍历
            path.pop_back();
        }
    }
};

3.3 给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
DFS查找专题_第8张图片

class Solution {
public:
    vector<vector<int> > combinationSum2(vector<int> &num, int target) {
        vector<int> path;
        set<vector<int>> res;
        sort(num.begin(),num.end());
        dfs(num,target,0,path,res);
        return vector<vector<int>> (res.begin(),res.end());
    }
    
    void dfs(vector<int> &num, int target,int start,vector<int> &path,set<vector<int>>&res){
        if(target<0)return ;
        if(target==0){
            res.insert(path);
            return;
        }
        
        
        for(int i=start;i<num.size();i++){
            if (target < num[i]) {
                break;
            }
            if (i > start && num[i] == num[i - 1]) {
                continue;
            }
            path.push_back(num[i]);
            dfs(num,target-num[i],i+1,path,res);
            path.pop_back();
        }
    }
};

3.4 给定数字1到9,从中选 k 个数,不考虑顺序,使得它们的和等于 n,返回所有方案。要求方案中不包含相同数字,且答案中不包含相同的方案。

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;

    vector<vector<int>> combinationSum3(int k, int n) {
        dfs(k, n, 1);
        return ans;
    }

    void dfs(int k, int n, int start)
    {
        if (!k)
        {
            if (!n) ans.push_back(path);
            return;
        }

        for (int i = start; i < 10; i ++ )
            if (n >= i)
            {
                path.push_back(i);
                dfs(k - 1, n - i, i + 1);//剩k-1个数,余数n-i,下一位第i+1位
                path.pop_back();
            }
    }
};
4 子集

4.1 给定一个集合,包含互不相同的数,返回它的所有子集(幂集)。
注意;结果不能包含相同子集。
DFS查找专题_第9张图片
(集合的二进制表示) O(2^n*n)
假设集合大小是 n,我们枚举每个数选与不选,一共 2^n个数。
另外,如果 n≥30,则 2^n≥ 10^9,肯定会超时,所以我们可以断定 n≤30,可以用int型变量来枚举。

时间复杂度分析:一共枚举 2^n 个数,每个数枚举 n 位,所以总时间复杂度是 O(2^n*n)。

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        int n = nums.size();
        for (int i = 0; i < (1 << n); i ++ )
        {
            vector<int> temp;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                    temp.push_back(nums[j]);
            res.push_back(temp);
        }
        return res;
    }
};

4.2 给定一个整数数组,可能包含重复元素。请返回它的所有子集(幂集)。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    
    vector<vector<int> > subsetsWithDup(vector<int> &S) {
        if(S.empty())return res;
        sort(S.begin(),S.end());
        dfs(S,0);
        return res;
    }
   
    void dfs(vector<int> &S,int start){
        res.push_back(path);//每次递归都算一种情况
        for(int i=start;i<S.size();i++){
            if(i>start&&S[i]==S[i-1])continue;
            path.push_back(S[i]);
            dfs(S,i+1);
            path.pop_back();
        }
    }
};
5 括号

5.1 给定括号对数 n,生成出所有合法的括号序列。
DFS查找专题_第10张图片
(直接生成合法的括号序列) O(Cn2n)
使用递归。
每次可以放置左括号的条件是当前左括号的数目不超过 nn。
每次可以放置右括号的条件是当前右括号的数目不超过左括号的数目。
时间复杂度
时间复杂度就是答案的个数,乘上保存答案的 O(n)计算量,该问题是经典的卡特兰数。
总时间复杂度为 O(n/n+1Cn2n)=O(Cn2n)

class Solution {
public:
    vector<string> res;
    void solve(int l, int r, int n, string cur) {
        if (l == n && r == n) {
            res.push_back(cur);
            return;
        }
        if (l < n)
            solve(l + 1, r, n, cur + "(");

        if (r < l)
            solve(l, r + 1, n, cur + ")");
    }
    vector<string> generateParenthesis(int n) {
        if (n == 0)
            return res;
        solve(0, 0, n, "");
        return res;
    }
};

5.2 给定一个嵌套的括号序列,含有 ( ) [ ] { } 三种括号,判断序列是否合法。

DFS查找专题_第11张图片
(栈结构) O(n)

  • 遇到左括号,需要压栈。
  • 遇到右括号,判断栈顶是否和当前右括号匹配;若不匹配则返回false,否则匹配弹出栈顶。
  • 最后判断栈是否为空;若为空则合法,否则不合法。
class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        for (int i = 0; i < s.length(); i++) {
            if (s[i] == '(' || s[i] == '[' || s[i] == '{')
                stk.push(s[i]);
            else if (s[i] == ')') {
                if (stk.empty() || stk.top() != '(')
                    return false;
                stk.pop();
            }
            else if (s[i] == ']') {
                if (stk.empty() || stk.top() != '[')
                    return false;
                stk.pop();
            }
            else {
                if (stk.empty() || stk.top() != '{')
                    return false;
                stk.pop();
            }
        }
        return stk.empty();
    }
};

5.3 给你一个由 ‘(’、’)’ 和小写字母组成的字符串 s。
你需要从字符串中删除最少数目的 ‘(’ 或者 ‘)’ (可以删除任意位置的括号),使得剩下的括号字符串有效。请返回任意一个合法字符串。
DFS查找专题_第12张图片
方法一:(栈) O(n)

  • 使用栈来维护括号,同时开一个数组记录哪些位置是不合法的。
  • 遇到左括号,则当前位置进栈。遇到右括号,如果栈空,则当前位置不合法,否则栈顶出栈。
  • 最后如果栈不空,则栈中所有的位置标记为不合法。

时间复杂度
每个位置遍历常数次,故时间复杂度为 O(n)。
空间复杂度
需要 O(n)的空间存放栈,标记数组和答案字符串。

class Solution {
public:
    string minRemoveToMakeValid(string s) {
        int n = s.length();

        stack<int> st;
        vector<bool> v(n, true);

        for (int i = 0; i < n; i++) {
            if (s[i] == ')') {
                if (st.empty()) v[i] = false;
                else st.pop();
            } else if (s[i] == '(') {
                st.push(i);
            }
        }

        while (!st.empty()) {
            v[st.top()] = false;
            st.pop();
        }

        string ans;
        for (int i = 0; i < n; i++)
            if (v[i])
                ans += s[i];

        return ans;
    }
};

方法二 (两次线性扫描) O(n)

  • 两次线性扫描,第一次从左到右扫描。维护一个计数器,遇到左括号,计数器加 1,加入左括号。遇到右括号,如果计数器为 0,则不加入右括号。遇到字母直接加入。
  • 第二次在第一次统计出的数组上再进行一次扫描,从右到左扫描。维护一个计数器,遇到右括号,计数器加 1,加入右括号。遇到左括号,如果计数器为 0,则不加入左括号。遇到字母直接加入。
  • 两次扫描结束之后的答案就是最终的合法答案。

时间复杂度
两次线性扫描,故时间复杂度为 O(n)。
空间复杂度
需要 O(n)的空间记录中间和答案字符串。

class Solution {
public:
    string minRemoveToMakeValid(string s) {
        int n = s.length();
        string t;
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (s[i] == '(') {
                cnt++;
                t += s[i];
            } else if (s[i] == ')') {
                if (cnt > 0) {
                    cnt--;
                    t += s[i];
                }
            } else {
                t += s[i];
            }
        }

        cnt = 0;
        string ans;
        for (int i = t.length() - 1; i >= 0; i--)
            if (t[i] == ')') {
                cnt++;
                ans += t[i];
            } else if (t[i] == '(') {
                if (cnt > 0) {
                    cnt--;
                    ans += t[i];
                }
            } else {
                ans += t[i];
            }

        reverse(ans.begin(), ans.end());
        return ans;
    }
};

5.4 删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。
说明: 输入可能包含了除 ( 和 ) 以外的字符。
DFS查找专题_第13张图片
DFS+剪枝
首先我们要知道删除多少左括号和右括号,我们从左到右遍历一遍,如果遇到左括号,那么l ++代表当前未匹配的左括号,如果遇到右括号并且当前未匹配的左括号个数大于0,那么l --,说明当前右括号前面有与之匹配的左括号,如果未匹配的左括号个数等于0,说明当前这个括号是不合法的,r ++,最后l代表还没有匹配的左括号个数,也就是我们需要删除的个数。

接下来就是搜索+剪枝了。dfs函数参数分别为当前字符串、当前字符串已经遍历过的字符,当前字符串还需要删除多少个左括号和右括号。当不需要再删除括号的时候,判断当前字符串是否合法,如果合法就加入答案。

在扫描字符串时,如果遇到非括号字符直接跳过,遇到左右括号的时候如果还可以删除当前括号,那么就删除并递归求解。

剪枝策略1:多个连续相同的括号,我们只删除最左边的那个进行搜索,因为删除后序的括号得到的字符串也是一样的。

剪枝策略2:如果剩余字符数字已经小于还需要删除的字符个数,剪枝。(因为有非括号字符,所以还可以标记剩余的左括号和右括号个数是否小于当前要删除的个数,得到更早的剪枝)

class Solution {
public:
    vector<string> res;
    vector<string> removeInvalidParentheses(string s) {
        int l = 0,r = 0,n = s.length();
        for(int i = 0 ; i < n ; i ++)
        {
            if(s[i] == '(') l ++;
            if(l == 0 && s[i] == ')') r ++;
            else if(s[i] == ')') l --;
        }
        dfs(s,0,l,r);
        return res;
    }
    bool check(string s)
    {
        int cnt = 0,n = s.length();
        for(int i = 0 ; i < n ; i ++)
        {
            if(s[i] =='(') cnt ++;
            else if(s[i] == ')') cnt --;
            if(cnt <0) return false;
        }
        return cnt == 0;
    }
    void dfs(string s,int u,int l,int r)
    {
        if(l == 0 && r == 0)
        {
            if(check(s)) res.push_back(s);
            return;
        }
        int n = s.length();
        for(int i = u ; i < n ; i ++)
        {
            if(s[i] != '(' && s[i] != ')') continue;
            if(i == u || s[i] != s[i - 1])//重复连续的(或)则只删第一个,因为删哪个都一样
            {
                string cur = s;
                cur.erase(i,1);
                if(s[i] == '(' && l > 0) dfs(cur,i,l - 1,r);
                else if(s[i] == ')' && r > 0) dfs(cur,i,l,r - 1);
            }
        }
    }
};

5.5 给定一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。

DFS查找专题_第14张图片

class Solution {
public:
    int longestValidParentheses(string s) {
        stack<int> stk;
        stk.push(-1);//处理边界问题,
        int res=0;
        for(int i=0;i<s.size();i++){
            if(s[i]=='(')
                stk.push(i);
            else{
                stk.pop();//如果s[i] == ‘)’,那么弹出栈顶元素 (代表栈顶的左括号匹配到了右括号)
                if(stk.empty())stk.push(i);//比如第一个字母为‘)’时,栈需要弹栈-1后为0,
                res=max(res,i-stk.top());
            }
        }
        return res;
    }
};

方法二:
双向扫描。不需要使用额外空间

  • 第一遍从左往右扫描,left和right分别代表当前合法的左括号个数和右括号个数。遇到左括号left ++,遇到右括号right++,如果left = right,说明找到了一个合法的括号对,更新答案,如果left < right,说明后面怎么匹配都不可能合法了,此时把left和right置为0。
  • 但是这样对于((())的括号序列,得不到正确解,因此我们继续从右往左匹配一次。
  • 第二遍第一遍从右往左扫描,left和right分别代表当前合法的左括号个数和右括号个数。遇到左括号left ++,遇到右括号right++,如果left = right,说明找到了一个合法的括号对,更新答案,如果left > right,说明前面怎么匹配都不可能合法了,此时把left和right置为0。
    int longestValidParentheses(string s) {
        int res = 0,n = s.length(), left = 0,right = 0;
        for(int i = 0 ; i < n ; i ++)
        {
            if(s[i] == '(') left ++;
            else right ++;
            if(left == right) res = max(res,2 * right);
            if(left < right) {left = 0;right = 0;}
        }
        left = 0,right = 0;
        for(int i = n - 1;i >= 0; i --)
        {
            if(s[i] == ')') right ++;
            else left ++;
            if(left == right) res = max(res,2 * right);
            if(right < left) {left = 0;right = 0;}
        }
        return res; 
    }

5.6 两个有效括号字符串的最大嵌套深度
DFS查找专题_第15张图片
DFS查找专题_第16张图片
栈) O(n)
括号序列一般会联想到用栈来解决(左括号进栈,匹配右括号后出栈)

  • 这题的核心思想在于分组,需要把一个括号序列一分为二,且两个子序列的嵌套深度尽可能的接近(这样总嵌套深度就达到最小值)
  • 如何做到尽可能的接近呢?可以发现()()嵌套深度是1,(())嵌套深度是2,把这两个连续的左括号分到AB两组中,A:():B(),总嵌套深度是1。如果把连续出现的左括号分到不同的组中,就可以保证两组的括号序列的嵌套深度尽可能接近。
  • 如何用代码实现这个想法呢?定义一个变量表示栈的深度,也就是左括号的出现个数,如果左括号匹配到右括号,自然分成一组,如果左括号之后又是左括号,则分为不同的两组。
  • 这里需要注意的一点是:通过奇偶来保证是否为一组,需要在判断右括号时,先用深度来算出分组编号,再把左括号弹出。(这里说的栈不需要用真正的栈,只需要用一个变量来记录栈的深度即可)
class Solution {
public:
    vector<int> maxDepthAfterSplit(string seq) {
        int d = 0;
        vector<int> res;

        for (char c: seq){
            if (c == '('){
                d++;
                res.push_back(d % 2);
            }
            else {
                res.push_back(d % 2);
                d--;
            }
        }

        return res;
    }
};

5.7 请实现一个简易计算器,计算一个算数表达式的值。
表达式中仅包含左括号(、右括号)、加号+、减号-、非负整数 和空格
DFS查找专题_第17张图片
(栈,表达式求值) O(n)
开两个栈,一个记录数字,一个记录操作符。
然后从前往后扫描整个表达式:

  • 如果遇到 (、+、-,直接入栈;

  • 如果遇到数字,则判断操作符栈的栈顶元素,如果不是(,则弹出操作符的栈顶元素,并用相应操作更新数字栈的栈顶元素。从而保证操作符栈的栈顶最多有一个连续的+或-;

  • 如果遇到 ),此时操作符栈顶一定是 (,将其弹出。然后根据新栈顶的操作符,对数字栈顶的两个元素进行相应操作;
    时间复杂度分析:每个数字和操作进栈出栈一次,所以总时间复杂度是 O(n)O(n)。

class Solution {
public:
    void calc(stack<char> &op, stack<int> &num) {
        int x=num.top();
        num.pop();
        int y=num.top();
        num.pop();
        if(op.top()=='+')num.push(x+y);
        else num.push(y-x);
        op.pop();
    }

    int calculate(string s) {
        stack<int> num;
        stack<char> op;
        for(int i=0;i<s.size();i++){
            char c=s[i];
            if(c==' ')continue;
            if(c=='+'||c=='-'||c=='(')
                op.push(c);
            else if(c==')'){
                op.pop();//此时栈顶元素一定是(
                if(op.size()&&op.top()!='(')
                    calc(op,num);
            }
            else {
                int j = i;
                while (j < s.size() && isdigit(s[j])) j ++ ;
                num.push(atoi(s.substr(i, j - i).c_str()));
                i = j - 1;
                if (op.size() && op.top() != '(') {
                    calc(op, num);
                }
            }
        }
        return num.top();
    }
};

5.8 实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式仅包含非负整数,+, - ,*,/ 四种运算符和空格 。 整数除法仅保留整数部分。

(栈模拟) O(n)

  • 首先我们用两个栈,一个数字栈保存数字和中间结果,一个符号栈保存遇到的操作符,然后我们是在遇见操作数的时候对表达式进行求值。
  • 比如对于"1 + 2 * 3"我们遇见2的时候不能先计算栈顶的加号,因为并不能确定后面还有没有优先级更高的乘除号,但是对于"1 + 2 + 3 * 4",我们遇见3前面的+号的时候我们就确定可以计算1 + 2了,这样我们始终维护操作符栈顶不会出现多与一个加减号而且栈的大小不会多于2。
  • 扫描完后栈里可能还剩有一个加减号,比如"1 + 2 + 3",我们可以在字符串后面补上一个加减号来使得扫描完字符串后,数字栈的栈顶就是答案。
class Solution {
public:
    int calculate(string s) {
        s += "+";
        stack<int> nums;
        stack<char> op;
        for (int i = 0; i < s.size(); i ++ ) {
            if (s[i] == ' ') continue;
            if (s[i] == '*' || s[i] == '/') op.push(s[i]);
            else if (s[i] == '+' || s[i] == '-') {
                if (op.size()) {
                    calc(nums, op);
                }
                op.push(s[i]);
            } else {
                int j = i;
                int n = 0;
                while (j < s.size() && isdigit(s[j])) {
                    n = n * 10 + (s[j] - '0');
                    j ++ ;
                }
                i = j - 1;
                nums.push(n);
                if (op.size() && (op.top() == '*' || op.top() == '/')) {
                    calc(nums, op);
                } 
            }
        }
        return nums.top();
    }

    void calc(stack<int> &nums, stack<char> &op) {
        int n2 = nums.top(); nums.pop();
        int n1 = nums.top(); nums.pop();
        char c = op.top(); 
        op.pop();
        if (c == '+') nums.push(n1 + n2);
        else if (c == '-') nums.push(n1 - n2);
        else if (c == '*') nums.push(n1 * n2);
        else nums.push(n1 / n2);
    }
};

5.9 计算逆波兰表达式的值。
表达式中的运算符仅包含 +,-,*,/。
注意:
/表示整除运算;
给定的逆波兰表达式一定合法。即不会导致除零运算。
DFS查找专题_第18张图片
(栈操作) O(n)
遍历所有元素。如果当前元素是整数,则压入栈;如果是运算符,则将栈顶两个元素弹出做相应运算,再将结果入栈。
最终表达式扫描完后,栈里的数就是结果。
时间复杂度分析:每个元素仅被遍历一次,且每次遍历时仅涉及常数次操作,所以时间复杂度是 O(n)

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> sta;
        for (auto &t : tokens)
            if (t == "+" || t == "-" || t == "*" || t == "/")
            {
                int a = sta.top();
                sta.pop();
                int b = sta.top();
                sta.pop();
                if (t == "+") sta.push(a + b);
                else if (t == "-") sta.push(b - a);
                else if (t == "*") sta.push(a * b);
                else sta.push(b / a);
            }
            else sta.push(atoi(t.c_str()));
        return sta.top();
    }
};
6 .数独

6.1 判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
DFS查找专题_第19张图片
(位运算判重)
分别使用一个整型数组记录每行、每列和每个九宫格内数字的存在情况。
位运算可以极大的简化判断,提高效率,具体看代码。

class Solution {
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        vector<int> row(9), col(9), squ(9); // 使用三个整型数组判重。
        for (int i = 0; i < 9; i++)
            for (int j = 0; j < 9; j++) {
                if (board[i][j] == '.')
                    continue;
                if (board[i][j] < '1' || board[i][j] > '9') return false;
                int num = board[i][j] - '0';

                // 以row[i] & (1 << num)为例,这是判断第i行中,num数字是否出现过。
                // 即row[i]值的二进制表示中,第num位是否是1。
                // 以下col和squ同理。

                if ((row[i] & (1 << num)) ||
                    (col[j] & (1 << num)) ||
                    (squ[(i / 3) * 3 + (j / 3)] & (1 << num)))
                    return false;

                row[i] |= (1 << num);
                col[j] |= (1 << num);
                squ[(i / 3) * 3 + (j / 3)] |= (1 << num);
            }
        return true;
    }
};

6.2 编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。

(递归回溯)

  • 首先按照 Valid Sudoku 的方法,预处理出 col、row 和 squ 数组。
  • 从 (0,0) 位置开始尝试并递归。遇到 . 时,枚举可以填充的数字,然后判重并加入 col、row 和 squ 数组中。
  • 如果成功到达结尾,则返回 true,告知递归可以终止。
class Solution {
public:
    bool solve(int x, int y, vector<vector<char>>& board,
            vector<int>& row, vector<int>& col, vector<int>& squ) {
        if (y == 9) {
            x++;
            y = 0;
        }

        if (x == 9)
            return true;

        if (board[x][y] == '.') {
            for (int i = 1; i <= 9; i++)
                if (! (
                    (row[x] & (1 << i)) ||
                    (col[y] & (1 << i)) ||
                    (squ[(x / 3) * 3 + (y / 3)] & (1 << i))
                )) {
                    row[x] |= (1 << i);
                    col[y] |= (1 << i);
                    squ[(x / 3) * 3 + (y / 3)] |= (1 << i);
                    board[x][y] = i + '0';

                    if (solve(x, y + 1, board, row, col, squ))
                        return true;

                    board[x][y] = '.';
                    row[x] -= (1 << i);
                    col[y] -= (1 << i);
                    squ[(x / 3) * 3 + (y / 3)] -= (1 << i);
                }
        } else {
            if (solve(x, y + 1, board, row, col, squ))
                return true;
        }

        return false;
    }

    void solveSudoku(vector<vector<char>>& board) {
        vector<int> row(9), col(9), squ(9);
        for (int i = 0; i < 9; i++)
            for (int j = 0; j < 9; j++) {
                if (board[i][j] == '.')
                    continue;

                int num = board[i][j] - '0';
                row[i] |= (1 << num);
                col[j] |= (1 << num);
                squ[(i / 3) * 3 + (j / 3)] |= (1 << num);
            }

        solve(0, 0, board, row, col, squ);
    }
};
7.n皇后

7.1 n-皇后问题是将 n 个皇后放在 n∗n 的棋盘上,使得皇后不能相互攻击到
DFS查找专题_第20张图片
为了优化时间效率,定义 vectorrow, col, diag, anti_diag;,用来记录每一行、每一列、每条对角线上是否有皇后存在。
搜索时需要记录4个状态:x,y,s,nx,y,s,n,分别表示横纵坐标、已摆放的皇后个数、棋盘大小。
对于每步搜索,有两种选择:

  • 当前格子不放皇后,则转移到 dfs(x, y + 1, s, n);
  • 如果 (x,y)(x,y) 所在的行、列、对角线不存在皇后,则当前格子可以摆放皇后,更新row, col, diag, anti_diag后转移到 dfs(x, y + 1, s + 1, n);
  • 回溯时不要忘记恢复row, col, diag, anti_diag等状态。

时间复杂度分析:
由于 nn 个皇后不能在同行同列,所以每行恰有一个皇后,我们计算一下在不考虑对角线的情况下,方案数的上限:第一行有 n个位置可选,第二行有 n−1个位置可选,依次类推,可得方案数最多是 n!。所以时间复杂度是 O(n!)

class Solution {
public:
    vector<vector<string>> ans;
    vector<string> path;
    vector<bool> row, col, diag, anti_diag;

    vector<vector<string>> solveNQueens(int n) {
        row = col = vector<bool>(n, false);
        diag = anti_diag = vector<bool>(2 * n, false);
        path = vector<string>(n, string(n, '.'));
        dfs(0, 0, 0, n);
        return ans;
    }

    void dfs(int x, int y, int s, int n)
    {
        if (y == n) x ++ , y = 0;
        if (x == n)
        {
            if (s == n) ans.push_back(path);
            return ;
        }

        dfs(x, y + 1, s, n);
        if (!row[x] && !col[y] && !diag[x + y]
                && !anti_diag[n - 1 - x + y])
        {
            row[x] = col[y] = diag[x + y]
                = anti_diag[n - 1 - x + y] = true;
            path[x][y] = 'Q';
            dfs(x, y + 1, s + 1, n);
            path[x][y] = '.';
            row[x] = col[y] = diag[x + y]
                = anti_diag[n - 1 - x + y] = false;
        }
    }
};

8.1 给定一个 n∗m的矩阵,对于其中是0的元素,把它所在的整行整列都置成0。
请使用 原地算法,即只能使用额外 O(1)的空间。

DFS查找专题_第21张图片
(原地算法) O(nm)
我们只需统计出矩阵中每一行或者每一列是否有0,然后把含有0的行或者列都置成0即可。

用两个变量记录第一行和第一列是否有0。
遍历整个矩阵,用矩阵的第一行和第一列记录对应的行和列是否有0。
把含有0的行和列都置成0。
时间复杂度分析: 矩阵中每个元素只遍历常数次数,所以时间复杂度是 (nm)(nm)。
空间复杂度分析: 只用了两个额外的变量记录第一行和第一列是否含有0,所以额外的空间复杂度是 (1),满足原地算法的要求。

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        if (matrix.empty()) return;
        int n = matrix.size(), m = matrix[0].size();
        int col0 = 1, row0 = 1;
        for (int i = 0; i < n; i ++ )
            if (!matrix[i][0]) col0 = 0;
        for (int i = 0; i < m; i ++ )
            if (!matrix[0][i]) row0 = 0;
        for (int i = 1; i < n; i ++ )
            for (int j = 1; j < m; j ++ )
                if (!matrix[i][j])
                {
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                }
        for (int i = 1; i < n; i ++ )
            if (!matrix[i][0])
                for (int j = 1; j < m; j ++ )
                    matrix[i][j] = 0;

        for (int i = 1; i < m; i ++ )
            if (!matrix[0][i])
                for (int j = 1; j < n; j ++ )
                    matrix[j][i] = 0;

        if (!col0)
            for (int i = 0; i < n; i ++ )
                matrix[i][0] = 0;

        if (!row0)
            for (int i = 0; i < m; i ++ )
                matrix[0][i] = 0;
    }
};

8.2 给定一些整数,代表火柴棍的长度。求这些火柴棍是否可以组成一个正方形。火柴棍不可以拆分,但是可以拼接。

DFS搜索+剪枝。
DFS函数参数:k已经拼好了几根火柴,cur当前在拼的这根火柴已经拼了多长了,state,因为最多只有15根火柴,所以将其状态用二进制来表示,火柴数组以及数组长度。

搜索策略:如果k = 4,返回true;如果cur=target说明找到了一根新的火柴;否则:遍历所有可拼接的火柴(未被使用过并且拼上去之后不会大于目标长度的火柴)。

四种剪枝方法:

  1. 从大到小枚举所有边
  2. 每条边的内部木棍长度从大到小填
  3. 如果当前木棍填充失败,那么跳过接下来所有相同长度的木棍
  4. 如果当前木棍填充失败,并且是当前边的第一个,则直接剪掉当前分支
  5. 如果当前木棍填充失败,并且是当前边的最后一个,则直接剪掉当前分支
class Solution {
public:
    bool makesquare(vector<int>& nums) {
        int n = nums.size(),sum = 0;
        for(int i = 0 ; i < n ; i ++)
            sum += nums[i];
        if(n < 4 || sum % 4 != 0) return false;
        sort(nums.begin(),nums.end(),greater<int>());
        return dfs(0,0,0,sum / 4,nums,n);
    }
    bool dfs(int k,int cur,int state,int target,vector<int>& nums,int n)
    {
        if(k == 4) return true;
        if(cur == target)
            return dfs(k + 1,0,state,target,nums,n);
        for(int i = 0 ; i < n ; i ++)
        {
            if(cur + nums[i] > target)
                continue;
            if(((state >> i) & 1) == 0)
            {
                state = state | (1 << i);
                if(dfs(k,cur + nums[i],state,target,nums,n))
                    return true;
                state = state & ~(1 << i);
                //如果当前木棍填充失败,那么跳过接下来所有相同长度的木棍
                while(i+1<n&&nums[i+1]==nums[i])i++;
 //如果在cur=0的时候就失败了,直接返回false,说明还没有使用的最长的火柴找不到可匹配的组合。
                if(cur == 0) return false;
                //如果当前木棍填充失败,并且是当前边的最后一个,则直接剪掉当前分支
                //这是因为我们已经将火柴降序排列,那么后序搜索的火柴和当前cur的和要小于target
                if(cur + nums[i] == target) return false;
            }
        }
        return false;
    }
};

8.3 (Number of Islands) 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

(深度优先遍历) O(nm)

  • 从任意一个陆地点开始,即可通过四连通的方式,深度优先搜索遍历到所有与之相连的陆地,即遍历完整个岛屿。每次将遍历过的点清 0。
  • 重复以上过程,可行起点的数量就是答案。
class Solution {
public:
    int dy[4]={-1,0,1,0},dx[4]={0,1,0,-1};

    void dfs(vector<vector<char>>& grid,int x,int y){
        int n=grid.size(),m=grid[0].size();
        grid[x][y]='0';
        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||grid[a][b]=='0')
                continue;
            dfs(grid,a,b);
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int n=grid.size();
        if(!n)return 0;
        int m=grid[0].size();
        
        int res=0;
        for(int i=0;i<n;i++)
            for(int j=0;j<m;j++){
                if(grid[i][j]=='1'){
                    res++;
                    dfs(grid,i,j);
                }
            }
        return res;
    }
};

8.4(Surrounded Regions) 给定一个二维地图,仅包含’X’和’O’(字母O),请攻占所有被’X’包围的’O’。一片区域被攻占,则将这片区域的’O’变成’X’。

深度优先遍历O(n^2)
逆向考虑问题,我们先统计出哪些区域不会被攻占,然后将其它区域都变成’X’即可。
具体做法如下:

  • 开一个二维布尔数组,记录哪些区域被遍历过。
  • 枚举所有边界上的’O’,从该位置做Flood Fill,即做深度优先遍历,只遍历是’O’的位置,并将所有遍历到的位置都标记成true。
  • 将所有未遍历到的位置变成’X’。

时间复杂度分析:每个位置仅被遍历一次,所以时间复杂度是 O(n2),其中 n 是地图的边长。

class Solution {
public:
    vector<vector<bool>> st;
    int n, m;

    void solve(vector<vector<char>>& board) {
        if (board.empty()) return;
        n = board.size(), m = board[0].size();
        st=vector<vector<bool>> (n,vector<bool>(m,false));

        for (int i = 0; i < n; i ++ )
        {
            if (board[i][0] == 'O') dfs(i, 0, board);
            if (board[i][m - 1] == 'O') dfs(i, m - 1, board);
        }
        for (int i = 0; i < m; i ++ )
        {
            if (board[0][i] == 'O') dfs(0, i, board);
            if (board[n - 1][i] == 'O') dfs(n - 1, i, board);
        }

        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ )
                if (!st[i][j])
                    board[i][j] = 'X';
    }

    void dfs(int x, int y, vector<vector<char>>&board)
    {
        st[x][y] = true;
        int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
        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 && !st[a][b] && board[a][b] == 'O')
                dfs(a, b, board);
        }
    }
};

你可能感兴趣的:(DFS查找专题)