Leetcode回溯算法经典题目总结

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。


(来源:https://leetcode-cn.com/tag/backtracking/)

参考内容:

https://leetcode-cn.com/problems/palindrome-partitioning/solution/hui-su-you-hua-jia-liao-dong-tai-gui-hua-by-liweiw/

https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/

https://leetcode-cn.com/problems/permutations/solution/jspython-hui-su-tao-lu-mo-ban-ti-46-quan-pai-lie-b/

目录

回溯算法模板

回溯算法典型题目

46. 全排列(回溯法模板)

39. 组合总和(改变起始点进行剪枝)

40. 组合总和 II (改变起始点,约束同层重复元素进行剪枝)

78. 子集(改变起始点)

90. 子集 II(约束同层重复元素进行剪枝)

面试题38. 字符串的排列

131. 分割回文串

47. 全排列 II(同层及垂直层屏蔽相同内容进行剪枝)

二维平面上的回溯问题

51. N皇后

52. N皇后 II

剪枝技巧总结

标志位

规定起始点

跨层剪枝

复杂回溯算法

22. 括号生成

301. 删除无效的括号

17. 电话号码的字母组合


回溯算法模板

回溯算法的三个要素:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

(作者:labuladong链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/)

回溯算法的框架:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

作者:labuladong
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表移除
    路径.add(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

作者:labuladong
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

具体的细节内容请查看labuladong大佬的题解或者公众号

回溯算法典型题目

46. 全排列(回溯法模板)

https://leetcode-cn.com/problems/permutations/

本题可以说是回溯法的模板问题了,列举出所有的可能,我们会发现,整个排列的可能性成树装,看图:

首先在1,2,3中进行选择,然后在剩下的没有被选的内容中,进行选择

这个描述过程本身就是有剪枝的性质在,我们要选择路径中不存在的内容,路径中重复的内容,我们不选择。

跨层剪枝技巧一

出现在路径中的元素,我们不选择,规避重复选择。

此技巧非常有针对性,原数组必须没有重复元素,非常方法直接失效,比如全排列II

if(find(track.begin(),track.end(),nums[i])==track.end())//原数组无重复元素,列举全部排列组合的剪枝方法

下面我们来看完整版的程序

class Solution {
public:
    vector<vector<int>> res;
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> track;
        backtrack(track,nums);
        return res;
    }
    void backtrack(vector<int>& track,vector<int>& nums)
    {
        //结束条件——何时完成选择
        if(track.size() == nums.size())//路径满足排列的要求
        {
            res.push_back(track);
            return;
        }
        //回溯的核心,选择与撤回
        for(int i = 0;i<nums.size();++i)
        {
            if(find(track.begin(),track.end(),nums[i])==track.end())//没有找到,那么就可以选择
            {
                track.push_back(nums[i]);
                backtrack(track,nums);
                track.pop_back();
            }
        }
    }
};

 

从本题可以看出,回溯法的精髓,在于“选择”和“撤销选择”

 

但是有时候决定命运的不是方向,是细节,二分查找被称为玄学算法,就是边界收缩的细节千变万化,模板套路远不及题目灵活

回溯法也有异曲同工之处,那就是剪枝,如何剪枝,如何避免重复的答案,非常重要,下面我们就看看几道经典的剪枝题目:

 

 

39. 组合总和(改变起始点进行剪枝)

https://leetcode-cn.com/problems/combination-sum/

如果我们直接写,程序如下:

class Solution {
public:
    vector<vector<int>> Res;
    unordered_map<int,int>M;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<int>track;
        backtrack(track,candidates,target);
        return Res;
    }
    void backtrack(vector<int>& track,vector<int>& candidates, int target)
    {
        if(target == 0) 
        {
            Res.push_back(track);
            return;
        }
        for(int i = 0;i<candidates.size();++i)
        {
            int newtarget = target-candidates[i];
            if(newtarget>=0) 
            {
                track.push_back(candidates[i]);
                backtrack(track,candidates,newtarget);
                track.pop_back();
            }
            
        }
    }
};

显然,有很大重复排列的结果,我们在过程中应该如何剪枝呢?

这道题目要求,可以重复选择元素,那么我们使用不选择路径中已经有数值的办法就失效了

那么我们该如何减去重复的组合呢?先要看看重复的组合是怎么来的:

图中蓝色为可提供的选择,黑色为目前已经选择的数值

可以看到,在不加任何限制的情况下,每次递归都有同样的选择,2,3,6。

解决办法就是:规定起始点位置。每次只能选择大于或等于上次选择元素序号的内容,看代码更好理解

代码实现:

        for(int i = begin;i<candidates.size();++i)//(1)
        {
            int newtarget = target-candidates[i];
            if(newtarget>=0) 
            {
                track.push_back(candidates[i]);
                backtrack(track,candidates,newtarget,i);//(2)
                track.pop_back();
            }
            
        }

本次选择从begin开始,那么下次选择也是从begin开始,如果begin为1(1代表元素索引),那么下次选择只能选择索引大于或等于1的内容,及能选择重复内容(符合题意),也能剪枝,我们来看看这么写之后,决策树有什么变化:

显而易见,我们成功完成了剪枝的工作,改变每次选取值的起点,既然要求可以重复选择,那么我们让下次的起点,从我们已经选择了的数字开始,而不是从头开始。

参考解法:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/

class Solution {
public:
    vector<vector<int>> Res;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<int>track;
        backtrack(track,candidates,target,0);
        return Res;
    }
    void backtrack(vector<int>& track,vector<int>& candidates, int target,int begin)
    {
        if(target == 0) 
        {
            Res.push_back(track);
            return;
        }
        for(int i = begin;i<candidates.size();++i)
        {
            int newtarget = target-candidates[i];
            if(newtarget>=0) 
            {
                track.push_back(candidates[i]);
                backtrack(track,candidates,newtarget,i);
                track.pop_back();
            }
            
        }
    }
};

 

40. 组合总和 II (改变起始点,约束同层重复元素进行剪枝

https://leetcode-cn.com/problems/combination-sum-ii/

本题相较上一题,提出了“每个数字在每个组合只能使用一次” 的要求,但是数组中有重复元素的。

那么策略也很简单,综合全排列和组合总和的剪枝方法,因为有数值中有重复元素,不能使用全排列的方法,此时要求每个数字只能使用一次,那么我们完全可以从每次选择的起点入手,本次选择了索引为x的内容,那么下次就从x+1开始好了,这样包装数组中的元素只用一次,此方法需要保证原数组是有序的

下面是完整代码,我们需要先对原数组进行排序,然后每次都将可选择起始点后移

class Solution {
public:
    vector<vector<int>> Res;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<int> track;
        sort(candidates.begin(),candidates.end());
        backtrack(track,candidates,target,0);
        return Res;
    }
    void backtrack(vector<int>& track,vector<int>& candidates, int target,int begin)
    {
        if(target == 0)
        {
            Res.push_back(track);
            return;
        }
        vector<bool> used(candidates.size(),false);
        for(int i = begin;i<candidates.size();++i)
        {
            if(used[candidates[i]]) continue;
            int temp = target - candidates[i];
            if(temp>=0)
            {
                used[candidates[i]] = true;
                track.push_back(candidates[i]);
                backtrack(track,candidates,temp,i+1);
                track.pop_back();
            }
        }

    }
};

 

78. 子集改变起始点

https://leetcode-cn.com/problems/subsets/

此题要求所有的子集,那么其实更好办,原始数组没有重复元素,显然子集中也不能有重复内容,依旧让起点索引点不断加一,每个点只能选择一次,即可完成此题。

class Solution {
public:
    vector<vector<int>> Res;
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<int> target;
        Res.push_back(target);
        backtrack(target,nums,0);
        return Res;
    }
    void backtrack(vector<int>& target,vector<int>& nums,int begin)
    {
        if(target.size()>0&&target.size()<=nums.size())
        {
            Res.push_back(target);
        }
        for(int i = begin;i<nums.size();++i)
        {
            target.push_back(nums[i]);
            backtrack(target,nums,i+1);
            target.pop_back();
        }

    }
};

 

90. 子集 II约束同层重复元素进行剪枝

https://leetcode-cn.com/problems/subsets-ii/

增加难度,原数组中有重复元素,老方法不好用了,会产生重复,如下图:

因为要收录全部的子集,所以我们也放宽了收录条件,造成了以上重复的情况

重复情况很有规律,都是同层使用了前一次使用的元素,所以本题的核心是同层剪枝。

书写标志位,Used【i】,这个元素同层用过,那么就不能用了。因为只是牵扯到同层的问题,所以不需要将其作为参数,只要在同层其作用即可。同样的,本题要求原数组从小到大排列,否则方法是失效的。

在上一道题目的基础上,增减同层标志位进行剪枝

代码如下:

        unordered_map<int,bool> used;
        for(int i = begin;i<nums.size();++i)
        {
            if(used[nums[i]]) continue;
            used[nums[i]]  = true;
            target.push_back(nums[i]);
            backtrack(target,nums,i+1);
            target.pop_back();
        }
class Solution {
public:
    vector<vector<int>> Res;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<int> target;
        sort(nums.begin(),nums.end());//排序
        Res.push_back(target);
        backtrack(target,nums,0);
        return Res;    
    }
    void backtrack(vector<int>& target,vector<int>& nums,int begin)
    {
        if(target.size()>0&&nums.size()>=target.size())
        {
            Res.push_back(target);
        }
        unordered_map<int,bool> used;
        for(int i = begin;i<nums.size();++i)
        {
            if(used[nums[i]]) continue;
            used[nums[i]]  = true;
            target.push_back(nums[i]);
            backtrack(target,nums,i+1);
            target.pop_back();
        }

    }
};

如果不排序,会有如下情况发生:

还是发生了重复,所以要排序,才能完全剪枝。

面试题38. 字符串的排列

https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/

典型的回溯问题,因为是字符串,而且有可能重复,所以需要同层和垂直层的剪枝。

具体代码如下: 

class Solution {
public:
    vector<string> Res;
    vector<string> permutation(string s) {
        //标准回溯
        if(s.empty()) return Res;
        string temp;
        unordered_map<int,int>G_item;
        backtrack(s,temp,G_item);
        return Res;
    }
    void backtrack(string s,string temp,unordered_map<int,int>& G_item)
    {
        if(s.size() == temp.size()) {Res.push_back(temp);return;}
        int size = s.size();
        unordered_map<char,int>Item;
        for(int i = 0;i<size;++i)
        {
            if(Item[s[i]]!=1&&G_item[i]!=1)//没有找到
            {
                Item[s[i]] = 1;
                G_item[i] = 1;
                string now = temp;
                temp += s[i];
                backtrack(s,temp,G_item);
                temp = now;
                G_item[i] = 0;
            }
        }
    }
};

 

131. 分割回文串

https://leetcode-cn.com/problems/palindrome-partitioning/

本题需要注意的地方还是挺多的,首先,第一个STL技巧,判断回文:

    bool Jadge(string& s)
    {
        //利用反向迭代器
        return s == string(s.rbegin(),s.rend());
    }

因为要求是子串,所以虽然算法架构上和其他题目一致,但是细节还是有很大区别的。

我们将循环i,从数组中的索引号,变成字符串中的长度,从长度为1的字符串开始,循环判断回文,再从长度为2的字符串开始,判读是不是回文,一次类推,知道长度恰好等于字符串完整的长度。

整个过程非常巧妙。

 

这是一道非常好的题目。

图源及思路参考:https://leetcode-cn.com/problems/palindrome-partitioning/solution/hui-su-you-hua-jia-liao-dong-tai-gui-hua-by-liweiw/

我们靠直觉都知道,我们应该分1个字符是回文的情况,两个字符分割的情况,以此类推

但是具体怎么实现呢?那就是截断字符串

现在我们从1个字符串是回文开始判断,第一位是回文,那么我们直接将第一位截断,让剩下的字符串继续递归

什么时候递归停止?因为我们截断了字符串,那么当字符串本身长度为0的时候,自然不会进入循环,自动停止

核心代码:

    void bacltrack(vector<string>& track,string s)
    {
        if(s=="") Res.push_back(track);
        for(int i = 1;i<=s.length();++i)
        {
            string temp = s.substr(0,i);
            if(Jadge(temp))
            {
                track.push_back(temp);
                bacltrack(track,s.substr(i,s.length()-i));
                track.pop_back();
            }
        }
    }

举例说明程序的运行过程:

当i=1的时候,不断递归,知道最后字符串为0,这是一组,橘黄色的线已经标出

当回溯的时候,到达bab的时候,bab也是一个回文,如此,目前vector还有a,c,刚好组成一组回文分割

这个过程实在是巧妙

核心代码:

        if(s=="") Res.push_back(track);
        for(int i = 1;i<=s.length();++i)
        {
            string temp = s.substr(0,i);
            if(Jadge(temp))
            {
                track.push_back(temp);
                bacltrack(track,s.substr(i,s.length()-i));
                track.pop_back();
            }
        }

过程非常巧妙,及找全了所有的回文,也不会存在多余的情况。 

class Solution {
public:
    vector<vector<string>> Res;
    vector<vector<string>> partition(string s) {
        vector<string> track;
        bacltrack(track,s);
        return Res;
    }
    void bacltrack(vector<string>& track,string s)
    {
        if(s=="") Res.push_back(track);
        for(int i = 1;i<=s.length();++i)
        {
            string temp = s.substr(0,i);
            if(Jadge(temp))
            {
                track.push_back(temp);
                bacltrack(track,s.substr(i,s.length()-i));
                track.pop_back();
            }
        }
    }
    bool Jadge(string& s)
    {
        //利用反向迭代器
        return s == string(s.rbegin(),s.rend());
    }
};

 

下面来看看最复杂的剪枝题目

47. 全排列 II(同层及垂直层屏蔽相同内容进行剪枝)

https://leetcode-cn.com/problems/permutations-ii/

(图源:liweiwei1419 https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/

重复元素如何避免:

同一层,重复元素不能使用 有:i>0 && !used[i-1] && nums[i] == nums[i-1];同层不能有人用你,上一层也不能有人用你

垂直层,重复元素不能使用 这个需要传参:used[i];表示下标为i的元素用过了,不要再使用了。

以上筛选条件,构成一组完美的剪枝条件

参考算法:https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/

class Solution {
public:
    vector<vector<int>>Res;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<int> temp;
        unordered_map<int,int>M;//垂直层剪枝(避免访问同一个元素)
        if(nums.size() == 0) return Res; 
        backtrack(nums,temp,M);
        return Res;
    }
    void backtrack(vector<int>& nums,vector<int>& temp,unordered_map<int,int>&M)
    {
        if(nums.size() == temp.size()) {Res.push_back(temp);return;}
        unordered_map<int,int>Used;//同层剪枝(避免相同元素)
        for(int i = 0;i<nums.size();++i)
        {
            if(Used[nums[i]]!=1&&M[i]!=1)
            {
                Used[nums[i]] = 1;//同层剪枝,用过标记为1,此处是用过的元素,为了同层剔除相同的元素
                M[i] = 1;//垂直层剪枝,用过标记为1,此处是用过元素的索引,为了避免重复,但是值相等是可以重复使用的
                temp.push_back(nums[i]);
                backtrack(nums,temp,M);
                temp.pop_back();
                M[i] = 0;//垂直层剪枝,用过标记为1
            }
        }
    }
};

二维平面上的回溯问题

二维回溯的思维模式和普通回溯法没有任何的区别,难就难在代码的实现,有时候让人摸不着头脑,不知该如何下手解决。

51. N皇后

https://leetcode-cn.com/problems/n-queens/

将此题简化为,放置N个物品,一个物品的横,竖,斜三个方向上,都不能放置其他物品

部分截图来源:https://leetcode-cn.com/problems/n-queens/solution/nhuang-hou-by-leetcode/

本题求解的是所有可能的解,这个要注意。

初探此题,简直是无法入手,这排列组合,谁知道,还不是要一步一步的试,这刚好就是回溯法的核心,如果第i个皇后没有地方放了,那么就悔棋一次,咱们试试其他地方,要是还不行,再次悔棋!

典型的回溯法思路,问题在于如何实现。

逻辑上我们会这么认为,从第一行开始,遍历每一列,然后放置皇后,标记出因为放置这个皇后而禁止放置任何皇后的区域,然后开始递归,从第二行开始。

第二行开始后,还是遍历每一列,排禁止放置的区域,在可以放置的区域进行放置,然后继续递归,第三行....

逻辑很好理解,那么应该如何实现呢?

算法参考:https://leetcode-cn.com/problems/n-queens/solution/hui-su-suan-fa-xiang-jie-by-labuladong/

首先初始化棋盘:

vector<string> board(n,string(n,'.'));

这是非常关键的一步,往后我们只需要放内容就可以了,不用担心其他内容

回溯的核心:

        for(int col = 0;col<size;++col)
        {
            //排除不合法
            if(Valid(board,row,col))
            {
                board[row][col] = 'Q';
                backtrack(board,row+1);
                board[row][col] = '.';
            }
        }

 我们直接修改数组,非常方便,进行落子即可,这就是初始化棋盘的方便所在,本题的核心可以说就是初始化这个部分

在此处落子是否可行,我们需要判断:

    bool Valid(vector<string>& board,int row,int col)
    {
        //检查列
        int n = board.size();
        for(int i = 0;i<n;++i)//固定列,检查所有行在此列上是否有Q元素
        {
            if(board[i][col] == 'Q') return false;
        }
        //检查左上方
        for(int i = row-1,j = col-1;i>=0&&j>=0;--i,--j)
        {
            if(board[i][j] == 'Q') return false;
        }
        //检查右上方
        for(int i = row-1,j = col+1;i>=0&&j<n;--i,++j)
        {
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }

检查上方和两侧斜角部分,有没有被其他旗子占用 

class Solution {
public:
    vector<vector<string>> Res;
    vector<vector<string>> solveNQueens(int n) {
        //初始化棋盘
        vector<string> board(n,string(n,'.'));
        backtrack(board,0);
        return Res; 
    }
    void backtrack(vector<string>& board,int row)
    {
        if(row == board.size()) {Res.push_back(board);return;}
        int size = board[row].size();
        for(int col = 0;col<size;++col)
        {
            //排除不合法
            if(Valid(board,row,col))
            {
                board[row][col] = 'Q';
                backtrack(board,row+1);
                board[row][col] = '.';
            }
        }
    }
    bool Valid(vector<string>& board,int row,int col)
    {
        //检查列
        int n = board.size();
        for(int i = 0;i<n;++i)//固定列,检查所有行在此列上是否有Q元素
        {
            if(board[i][col] == 'Q') return false;
        }
        //检查左上方
        for(int i = row-1,j = col-1;i>=0&&j>=0;--i,--j)
        {
            if(board[i][j] == 'Q') return false;
        }
        //检查右上方
        for(int i = row-1,j = col+1;i>=0&&j<n;--i,++j)
        {
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }
};

非常巧妙的解法,既然我们已经想到了回溯法,那么我们只需要列出架构,之后的种种细节,我们在也有架构的基础上进行。 

52. N皇后 II

https://leetcode-cn.com/problems/n-queens-ii/

如果我现在要求总共有几种解,你该怎么做呢?

class Solution {
public:
    int Res;
    int totalNQueens(int n) {
        //初始化棋盘
        vector<string> board(n,string(n,'.'));
        backtrack(board,0);
        return Res; 
    }
    void backtrack(vector<string>& board,int row)
    {
        if(row == board.size()) {Res++;return;}
        int size = board[row].size();
        for(int col = 0;col<size;++col)
        {
            //排除不合法
            if(Valid(board,row,col))
            {
                board[row][col] = 'Q';
                backtrack(board,row+1);
                board[row][col] = '.';
            }
        }
    }
    bool Valid(vector<string>& board,int row,int col)
    {
        //检查列
        int n = board.size();
        for(int i = 0;i<n;++i)//固定列,检查所有行在此列上是否有Q元素
        {
            if(board[i][col] == 'Q') return false;
        }
        //检查左上方
        for(int i = row-1,j = col-1;i>=0&&j>=0;--i,--j)
        {
            if(board[i][j] == 'Q') return false;
        }
        //检查右上方
        for(int i = row-1,j = col+1;i>=0&&j<n;--i,++j)
        {
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }
};

 

剪枝技巧总结

一般对数组有要求,必须是有序数组,这点需要保证

剪枝一般方法上述都有提到,下面做以总结,剪枝最好的办法就是找到为什么会重复,然后对症下药即可。

标志位

90. 子集 II是个非常好的例子,使用标志位,同层用过的内容,不会再次使用

        unordered_map<int,bool> used;
        for(int i = begin;i<nums.size();++i)
        {
            if(used[nums[i]]) continue;
            used[nums[i]]  = true;
            target.push_back(nums[i]);
            backtrack(target,nums,i+1);
            target.pop_back();
        }

规定起始点

当题目要求,可以重复选择同一个元素的时候,我们只用更新起始点就可以了,比如39. 组合总和,起始点需要大于等于上一次选取内容的索引即可。

78. 子集又是另外一种剪枝的方式,因为不允许使用重复元素,需要不断更新起始点才能保持完成剪枝

跨层剪枝

47. 全排列 II可以说是剪枝的集大成,甚至可以说这道题核心就是剪枝而不是回溯。既然要跨层,那么就必须要传递标致位,让下一次递归操作时知道,什么值改选什么不该。

 

复杂回溯算法

 

判断括号是否合法的常用方法:

    //以下为两组判断合法括号的方法
    bool Jadge(string s)
    {
        stack<char>Temp;
        for(int i = 0;i<s.size();++i)
        {
            if(s[i] == '(') Temp.push(s[i]);
            if(s[i] == ')')
            {
                if(Temp.size()&&Temp.top() == '(')  Temp.pop();
                //注意细节,Temp.size()不为0才能完成后续操作
                else return false;
            }
        }
        return Temp.size() == 0?true:false;
    }
    bool JadgeNum(string s)
    {
        int count = 0;
        for(int i = 0;i<s.size();++i)
        {
            if(s[i] == '(') count++;
            else if(s[i] == ')') count--;
            if(count<0) return false;
        }
        return count == 0?true:false;
    }

22. 括号生成

https://leetcode-cn.com/problems/generate-parentheses/

本题没有明确给出组成原始的内容,但是隐含在题意内,就是(和),整个过程中,就这两个元素,不断的进行组合

我们先看一段程序,是检查字符串是不是合法括号的:

    bool Jadeg(string s)//判断是不是括号
    {
        if(s.empty()) return false;
        stack<char> Text;
        for(auto item:s)
        {
            if(item == '(') Text.push('(');

            if(item == ')') 
            {
                if(Text.empty()||Text.top() != '(') return false;
                Text.pop();
            }
        }
        return Text.size()==0?true:false;
    }

那么我们来看完成版本的程序:

class Solution {
public:
    vector<string>Res;
    int size = 0;
    vector<string> generateParenthesis(int n) {
        if(n == 0) return Res;
        size = n;
        backtracck("");//目前已经组成的字符串,左右括号的个数
        return Res;
    }
    void backtracck(string target)
    {
        if(target.size() > 2*size) return;//大于最高尺寸后,直接返回停止递归
        if( Jadeg(target)&&target.size() == 2*size ) Res.push_back(target);

        //就两种情况,'('或者')',全部都给一遍即可
        string temp = target;//保存原始内容
        target+='(';backtracck(target);//左右都试一下
        target = temp;//恢复原样
        target+=')';backtracck(target);
        target = temp;//恢复原样

    }
    bool Jadeg(string s)//判断是不是括号
    {
        if(s.empty()) return false;
        stack<char> Text;
        for(auto item:s)
        {
            if(item == '(') Text.push('(');

            if(item == ')') 
            {
                if(Text.empty()||Text.top() != '(') return false;
                Text.pop();
            }
        }
        return Text.size()==0?true:false;

    }
};

但是本方法极限只能算到7

有没有什么办法,进行优化,我们能不能不使用之前的判断括号合理性的API,换一种更加合理和简单的方式?

以下两个方法都是通过左右括号的个数,来进行约束,完成合理组合的两种方法

改进方法一:

算法参考:https://leetcode-cn.com/problems/generate-parentheses/comments/6656

我们对左右括号进行计数,左括号个数为L,右括号个数为R,当二者相等且总长相等时,组成正确的括号表达式

同样的,当L或者R大于总括号数时,比如n =2,那么L和R极限大小就是2,当L和R大于这个数字时,直接break;

同样,当R大于L的时候,显然也是失效的,我们总是先添加左括号再添加右括号,L>=R是常态,当二者相等就是完成组合的时候

,所以当R大于L的时候,break;

以上条件必须全部具备,才能完全筛选出合理的组合

class Solution {
public:
    vector<string>Res;

    vector<string> generateParenthesis(int n) {
        if(n == 0) return Res;
        backtracck("",0,0,n);//目前已经组成的字符串,左右括号的个数
        return Res;
    }
    void backtracck(string target,int L,int R,int size)
    {
        if(L > size || R > size ||R > L||target.size() > 2*size) return;
        if(L == R&&target.size() == 2*size ) Res.push_back(target);

        //就两种情况,'('或者')',全部都给一遍即可
        string temp = target;//保存原始内容
        target+='(';backtracck(target,L+1,R,size);//左右都试一下
        target = temp;//恢复原样
        target+=')';backtracck(target,L,R+1,size);
        target = temp;//恢复原样

    }


};

改进方法二:

算法参考:https://leetcode-cn.com/problems/generate-parentheses/comments/336762

上面的约束很多,很容易乱,我们也没有其他办法剪枝,有的,版本一我们使用的是从无到有构建,版本二我们对括号个数在初期就进行约束

对L,R在递归初期就进行约束,当L大于0的时候,进行递归,但是只有当R>L的时候,也就是目前已经有左括号进入了组合,现在再去部署右括号才是合理

class Solution {
public:
    vector<string>Res;

    vector<string> generateParenthesis(int n) {
        if(n == 0) return Res;
        backtracck("",n,n,n);//目前已经组成的字符串,左右括号的个数
        return Res;
    }
    void backtracck(string target,int L,int R,int size)
    {
        // if(L > size || R > size ||target.size() > 2*size) return;
        if(L == R&&R == 0&&target.size() == 2*size ) Res.push_back(target);

        //就两种情况,'('或者')',全部都给一遍即可
        string temp = target;//保存原始内容
        if(L>0)//左括号剩余,那么拼接左括号
        {
            target+='(';backtracck(target,L-1,R,size);//左右都试一下
            target = temp;//恢复原样
        }
        if(R>L)//右括号剩余多余左括号剩余,那么可以尝试进行右括号的拼接
        {
            target+=')';backtracck(target,L,R-1,size);
            target = temp;//恢复原样
        }
    }
};

我们可以来看看,如果不进行R>L的约束,我们该如何?

这些都是多余的组合,这些组合都有一个问题,就是右括号出现在了左括号之前,所以一定要加以限制。

下面我们再看一道经典题目:

301. 删除无效的括号

https://leetcode-cn.com/problems/remove-invalid-parentheses/

思路参考:https://leetcode-cn.com/problems/remove-invalid-parentheses/solution/dfsjie-ti-by-hw_wt/ 

本题Hard,要求删除最小数量的无效括号,其实无效括号个个数已经是确定的,我们先找出非法括号,然后在这个字符串中尝试着删除括号,对删除完的内容进行判断,是否是有效的

首先我们先 统计非法括号的个数:

        //计算需要删除的错误左右括号个数
        for(auto item:s)
        {
            if(item == '(') left++;//记录全部的左括号
            else if(item == ')')
            {
                if(left>0) left--;//当遇到匹配的右括号时,删除一个,表示这个括号不在非法括号的范围内
                else right++;//一旦left 不大于0,但是此时出现了右括号,显然是非法括号
            }
        }

现在我们知道了要删除多少个左括号和右括号,这些都是非法的内容,我们从第一个字符开始,尝试删除

        for(int i = begin;i<s.size();++i)
        {
            if (i != begin && s[i] == s[i-1]) continue;//联系的左/右括号,不需要删除
            if (s[i] == '(' && left > 0)//尝试删除此处的左括号
            {
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left - 1, right);
            }
            if (s[i] == ')' && right > 0)//尝试删除此处的右括号
            {
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left, right - 1);
            }
        }

 完整代码如下:

class Solution {
public:
    vector<string>Res;
    vector<string> removeInvalidParentheses(string s) {
        int left = 0,right = 0;
        
        //计算需要删除的错误左右括号个数
        for(auto item:s)
        {
            if(item == '(') left++;//记录全部的左括号
            else if(item == ')')
            {
                if(left>0) left--;//当遇到匹配的右括号时,删除一个,表示这个括号不在非法括号的范围内
                else right++;//一旦left 不大于0,但是此时出现了右括号,显然是非法括号
            }
        }
        DFS(s, 0, left, right);
        return Res;
    }
    void DFS(string s,int begin,int left,int right)
    {
        if(left == right&&left == 0)
        {
            if(JadgeNum(s)) Res.push_back(s);
            // if(Jadge(s)) Res.push_back(s);
            return;
        }
        for(int i = begin;i<s.size();++i)
        {
            if (i != begin && s[i] == s[i-1]) continue;//联系的左/右括号,不需要删除
            if (s[i] == '(' && left > 0)//尝试删除此处的左括号
            {
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left - 1, right);
            }
            if (s[i] == ')' && right > 0)//尝试删除此处的右括号
            {
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left, right - 1);
            }
        }
    }
    //以下为两组判断合法括号的方法
    bool Jadge(string s)
    {
        stack<char>Temp;
        for(int i = 0;i<s.size();++i)
        {
            if(s[i] == '(') Temp.push(s[i]);
            if(s[i] == ')')
            {
                if(Temp.size()&&Temp.top() == '(')  Temp.pop();
                //注意细节,Temp.size()不为0才能完成后续操作
                else return false;
            }
        }
        return Temp.size() == 0?true:false;
    }
    bool JadgeNum(string s)
    {
        int count = 0;
        for(int i = 0;i<s.size();++i)
        {
            if(s[i] == '(') count++;
            else if(s[i] == ')') count--;
            if(count<0) return false;
        }
        return count == 0?true:false;
    }
};

DFS部分详解:

    void DFS(string s,int begin,int left,int right)
    {
        if(left == right&&left == 0)
        {
            if(Check(s)) Res.push_back(s);
            return;
        }
        for(int i = begin;i<s.size();++i)
        {
            //这个部分如果输入是())() 删除1和删除2,两种删除方法的结果一样,都是()()()
            //此处的判断是为了剪枝
            if (i != begin && s[i] == s[i-1]) continue;
            if (s[i] == '(' && left > 0)//尝试删除此处的左括号
            {
                //此处begin也是一个重要的细节,此处s为()())(),删除3位置的),那么下次应是从4位置的)
                //开始,但是传递给下一个递归的target已经删除了)(3位置),begin序号不能变,否则跳过一个
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left - 1, right);
            }
            if (s[i] == ')' && right > 0)//尝试删除此处的右括号
            {
                DFS(s.substr(0, i) + s.substr(i+1, s.size()-1-i), i, left, right - 1);
            }
        }
    }

本题难在,一般的回溯法都是内容的拼接和组合,本题是拆解,这是最难的部分。

17. 电话号码的字母组合

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/

本题极具技巧性,首先我们来看示例,23,是从2中选择一个数字,在3中也选择一个数字,然后进行组合

这并不是常规的回溯组合要求,我们目前所见到的都是同一串字符,不同位置的组合

但是本质还是一样的,我们看图:

在组合要求是2的时候,我们能够选择abc三个字母,到了选择3,我们需要选择def;

那么既然如此,我们就在递归的时候,规定本轮loop,我们是在怎么的组合要求下进行选择的即可

就像我们以前规定起点和终点一样,现在我们也是在规定选取范围

核心代码:

    void backtrack(string target,string digits,int begin)//begin代表了本轮的组合要求
    {
        if(target.size() == digits.size()) {Res.push_back(target);return;}
        string Here = Number[digits[begin]];//本轮要选取的内容
        for(auto item:Here)
        {
            string temp = target;
            target += item;
            backtrack(target,digits,begin+1);
            target = temp;
        }
    }

 下面对键盘进行初始化,为了方便,我们直接用Hash表进行优化:

unordered_map<char,string>Number;

//对整个键盘进行初始化
if(digits == "") return Res;
        
Number['2'] = "abc";Number['3'] = "def";
Number['4'] = "ghi";Number['5'] = "jkl";
Number['6'] = "mno";Number['7'] = "pqrs";
Number['8'] = "tuv";Number['9'] = "wxyz";

完整代码如下:

class Solution {
public:
    unordered_map<char,string>Number;
    vector<string> Res;
    vector<string> letterCombinations(string digits) {
        //对整个键盘进行初始化
        if(digits == "") return Res;
        // Number[0] = "abc";Number[1] = "def";
        Number['2'] = "abc";Number['3'] = "def";
        Number['4'] = "ghi";Number['5'] = "jkl";
        Number['6'] = "mno";Number['7'] = "pqrs";
        Number['8'] = "tuv";Number['9'] = "wxyz";

        int begin  = 0;
        backtrack("",digits,0);
        //第一个参数是目前已经完成的组合,参数二就是给定的组合要求,参数三是本轮付出怎么样的组合要
        return Res;
    }
    void backtrack(string target,string digits,int begin)
    {
        if(target.size() == digits.size()) {Res.push_back(target);return;}
        string Here = Number[digits[begin]];//本轮要选取的内容
        for(auto item:Here)
        {
            string temp = target;
            target += item;
            backtrack(target,digits,begin+1);
            target = temp;
        }
    }
};

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值