今天我们继续回溯:
39. 组合总和 - 力扣(LeetCode)
这个题和我们之前的组合题相比,最大的区别在于我们可以无限次的重复取用某值了,这就让我们的递归参数与之前不同,除此之外,本质上这个题与216. 组合总和 III - 力扣(LeetCode)区别并不大,我们依然是用回溯三步法来做:
第一步,确定终止条件,这里这个题除了基本的总和等于target以外,还需要考虑总和大于target,为什么之前不用考虑这个问题呢?因为之前我们不能重复取某值,所以我们只用考虑当总和等于target时将小容器的值返回大容器即可,因为哪怕我们的总和大于了target,只要不是等于target我们都会选择将小容器内元素进行更新。可是这个题因为可以无限次地重复取值,那么如果不加一个总和大于target的判断的话我们没有办法从无限次的重复取值中脱离造成超时。
第二步,我们需要进行遍历并处理节点,我们依然是维护一个总和并将节点放入小容器即可,不过 不同点在于我们不需要在递归时将序号更换为i+1而是i,因为我们现在可以无限次地重复取值,何时不再重复取值呢?当我们的总和大于target时。是的,这一步考虑总和大于target同时满足了我们两步的需求。
第三步,实现回溯,一样的总和减去当前元素且小容器删除当前元素。
代码如下:
class Solution {
public:vector<vector<int>> res;vector<int> temp;void backtrack(vector<int>& candidates, int target,int sum,int start){if(sum>target)return;if(sum==target){res.push_back(temp);return;}for(int i=start;i<candidates.size();++i){sum+=candidates[i];temp.push_back(candidates[i]);backtrack(candidates, target, sum, i);sum-=candidates[i];temp.pop_back();} }vector<vector<int>> combinationSum(vector<int>& candidates, int target) {backtrack(candidates, target, 0, 0);return res;}
};
在这个题中还涉及到了我们的startIndex的讨论,也就是什么时候我们需要去维护一个序号什么时候不用。
如何理解呢?其实很简单:序号是用来控制循环的开始位置的,什么时候我们需要去控制循环的开始位置呢?当我们要求在一个集合内求组合时,我们需要控制每个函数中循环的开始位置以避免重复,这个时候就需要去维护一个startIndex,可是在多个互相不影响的集合中,比如电话号码题,我们只需要在每个集合中取一个字符即可,那么我们就不需要这个序号。
40. 组合总和 II - 力扣(LeetCode)
这个组合题就是上一个组合题的对比版:这个题大集合中的每一个元素都只能取一次,但是对我们输出的结果有去重的要求。
那我们依然还是回溯:
第一步,确定终止条件,那么依然是我们的总和等于target,由于这个题没有无限次地重复取值,我们不用考虑sum大于target的情况。
第二步,遍历,由于是单集合且每个值只能取一次,我们需要一个startIndex来控制循环开始的位置。这里需要留意的有两点:第一是我们这个题希望输出是去重处理的,那具体的去重我们可以在最后进行也可以在遍历时就进行,一般来说我们肯定是希望在遍历时就解决最好。具体如何去重呢?最常见的方法就是我们将大集合排序后再判断当前遍历的值是否与上一个值相同,相同的话就跳过该次循环即可;第二是这个题的一些样例会出现超时的情况,要求我们去进行一些剪枝:什么是剪枝,就是我们可以减少一些不必要的运算。比如这个题中当我们发现目前的值已经大于target了就没必要继续这根子树的遍历了,所以我们在这里也需要添加这个步骤。
最后的回溯没有什么特殊之处,依然是去掉当前的值。
代码如下:
class Solution {
public:vector<vector<int>> res;vector<int> temp;void backtrack(int sum,int target,vector<int>& candidates,int startIndex){if(sum==target){res.push_back(temp);return;}for(int i=startIndex;i<candidates.size();++i){if(i>startIndex&&candidates[i]==candidates[i-1])continue;if(sum+candidates[i]>target)break;sum+=candidates[i];temp.push_back(candidates[i]);backtrack(sum, target, candidates, i+1);sum-=candidates[i];temp.pop_back();}}vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());backtrack(0, target, candidates, 0);return res;}
};
至此我们的组合问题就完成了,这五个题中,77. 组合 - 力扣(LeetCode)是最基础的组合问题,也是基本的思路体现。216. 组合总和 III - 力扣(LeetCode)在此基础上加入了对总和的要求,但是从代码和思想上都只是略加修改即可。17. 电话号码的字母组合 - 力扣(LeetCode)就要复杂一些,它不仅要求我们自己去创建哈希表建立数字与字符串的映射,还要求每个数字只能取一个字符,我们需要将这个层级关系整理得足够清晰才可以:我们先取数字,得到这个数字对应的字符串,这个字符串才是我们应该去回溯去遍历的对象,然后我们在for循环外面维护一个数字的字符串的序号去更新数字。39. 组合总和 - 力扣(LeetCode)则是允许无限重复取值,那么我们就需要多考虑sum大于target的情况不然会陷入无限循环,以及递归时不用去更新序号。40. 组合总和 II - 力扣(LeetCode)则是不允许无限取值但要求去重,这就要求我们对大集合进行排序并判断当前遍历元素是否与之前元素相同。
131. 分割回文串 - 力扣(LeetCode)
现在我们来到了分割问题,虽然叫法不一样,但是分割问题与组合问题其实差别没有那么大。
我们都知道组合问题是在一个大集合里找到符合要求的小集合,在遍历的过程中需要做的事就是去读取各个元素,而分割问题实际上也是类似的:我们依然需要维护一个序号,序号前的就是已经被截取的,而序号后的就是我们还没有截取的。
我们用两张图来体现差异:
这是组合问题。
而这就是分割问题,我们依然是用递归实现纵向遍历用for实现横向遍历,将问题抽象成树的遍历问题。
回到题目本身,我们需要不断地去分割字符串并将所有的回文串记录下来并返回。
首先我们依然是回溯的思想:
第一是终止条件:在分割问题中,我们的startIndex就是分割线,那么我们的终止条件就是当startIndex大于等于我们的大集合的大小时,这说明我们找到了所有的分割方法,可以将小容器放入大容器并返回了。
第二是遍历,在这个题中,我们需要去判断子串是否为回文串,首先我们需要一个辅助函数,最简单的就是通过左右指针来判断。然后当我们发现子串为回文串时,我们将子串放入小容器中,否则我们直接continue以跳过这根子树,我们就这样不断地更新分割位置即可(横向遍历)。
第三步是回溯,我们只需要在执行了小容器的push_back操作的基础上进行pop_back即可。
代码如下:
class Solution {
public:vector<vector<string>> res;vector<string> temp;void backtrack(string s,int startIndex){if(startIndex>=s.size()){res.push_back(temp);return;}for(int i=startIndex;i<s.size();++i){if(helper(s, startIndex, i)){string str=s.substr(startIndex,i-startIndex+1);temp.push_back(str);backtrack(s, i+1);temp.pop_back();}else continue;}}bool helper(const string& str,int start,int end){int l=start,r=end;while(l<r){if(str[l]!=str[r])return false;++l;--r;}return true;}vector<vector<string>> partition(string s) {backtrack(s, 0);return res;}
};