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

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

(来源: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(路径)returnfor 选择 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;}}
};

Leetcode回溯算法经典题目总结相关推荐

  1. 回溯算法的题目,这样做,秒杀!!

    点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了 3 个月总结的一线大厂 Java 面试总结,本 ...

  2. 八十四、Python | Leetcode回溯算法系列

    @Author:Runsen @Date:2020/7/7 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  3. n皇后问题python_N皇后问题—回溯算法经典例题

    N 皇后是回溯算法经典问题之一.问题如下:请在一个 ni n 的正方形盘面上布置 n 名皇后,因为每一名皇后都可以自上下左右斜方向攻击,所以需保证每一行.每一列和每一条斜线上都只有一名皇后. 最简单的 ...

  4. [LeetCode] 回溯算法

    回溯法 回朔法的思想: 通过枚举法,对所有可能性进行遍历. 但是和枚举法不同的是回溯法不是一直遍历下去,而是在不满足条件是回退一步,去尝试其余的路,而回退的这一步就是回溯算法的关键. 因此回朔法可以简 ...

  5. LeetCode回溯算法——51.N皇后问题详解

    51.N皇后 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子. n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击. 给你一个整 ...

  6. 力扣刷题记录-回溯算法相关题目

    首先介绍一下回溯算法 回溯通常在递归函数中体现,本质也是一种暴力的搜索方法,但可以解决一些用for循环暴力解决不了的问题,其应用有: 1.组合问题: 例:1 2 3 4这些数中找出组合为2的组合,有1 ...

  7. 回溯算法经典问题-迷宫问题

    迷宫问题是一道经典的回溯算法问题,给定一个迷宫矩阵,矩阵中的1表示障碍,0表示可走通路,给定迷宫入口出口,要求寻找从入口穿过迷宫到达出口的所有路径,有则输出,无则给出提示.一本合格的数据结构教科书一般 ...

  8. python回溯算法_回溯算法经典问题及python代码实现

    2. 0-1背包问题 # 0-1 bag problem import sys def f(no, cur_mass, things, num): global cur_max if no == nu ...

  9. leetcode回溯算法

    LeetCode--回溯法心得 ​ 武汉大学 软件工程硕士在读 ​关注他 206 人赞同了该文章 这两天在刷LeetCode37题解数独时,被这个回溯法折腾的不要不要的,于是我疼定思疼发誓一定要找个能 ...

最新文章

  1. Android 停止调试程序
  2. java 变量单例_Java静态变量的用法:伪单例
  3. ORACLE同义词源库锁表导致目标库删除操作报ora 02055 02049 02063 06512
  4. Spring 组cxf宣布webservice
  5. 两个排序数组的中位数
  6. 在ubuntu 16.04上安装tensorflow,并测试成功
  7. js+excel+mysql_js导出数据到excel
  8. 并查集一般高级应用的理解
  9. php多个逻辑如何分为多个逻辑块,php 项目如何分层
  10. 主成分分析法案例_主数据管理第一步——识别主数据
  11. idea创建maven工程_maven创建父子工程 springboot自动配置
  12. multisim 10.0安装、破解、汉化
  13. 【软件工具】--- 软件安装管家目录
  14. 【待更新】【Rockchip】瑞芯微/rockchip 开发环境搭建|编译|烧录 开发实例
  15. 京东 vs 苏宁:两个穷人的流血战争
  16. October 2006
  17. 微博消息分析-大数据项目
  18. swing界面如何增加日历功能
  19. idmp计算任务shell脚本创建路径全过程
  20. 不可抗力条款_否则,如果条款

热门文章

  1. uniapp小程序实现弹幕功能
  2. openpnp - camera - FPS掉帧的解决思路
  3. 使用Flutter重构斗鱼APP
  4. 【记录我的作业2】工业机器人职业技能训练——week3RobotStudio纪念币和键盘工作台仿真。
  5. @Transactional注解详解
  6. 拼多多2019秋招编程题——选靓号
  7. 输入体重和身高,判断输出体重类型
  8. IBM3650M4服务器RADI5更换硬盘
  9. iphone Simulator 路径
  10. Android 性能优化 - 彻底解决内存泄漏