个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:练题
长路漫漫浩浩,万事皆有期待
题目要求是可以使用重复的数据进行构造答案,整体思路和组合总和3没差多少,就只有一点细节需要注意。幸运的是这道题给的是一个无重复数字的数组,那么有什么区别吗?肯定是有区别的,有重数组不管我们要的答案是可以一个数字取很多次,还是不可以,都要进行数组去重。打个比方,我们数组里有两个2,目标值是4,那么我们可以一个数字取多次的话,比如用第一个2返回一组答案是{2,2},那么再用第二个数字2时候,我们取到的组合不是和用第一个数字取到的是一样的吗?这肯定不行,虽然我们可以一个数字重复用,但是最后返回的答案不能有相同的。
说完了这一点,我们直接看代码,看看和上一道题都有哪些不同
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtacking(vector<int>&candidates,int target,int sum,int startIndex)
{
if(sum>target)
{
return;
}
if(sum==target)
{
result.push_back(path);
return;
}
for(int i=startIndex;i<candidates.size();i++)
{
sum+=candidates[i];
path.push_back(candidates[i]);
backtacking(candidates,target,sum,i);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtacking(candidates,target,0,0);
return result;
}
};
实际上不同之处并没有很多,只有每次递归传入的是i,而并非i+1,这是因为我们的数组中元素可以重复选取,所以我们下一次传进来的还是i,那么我们如何选到其他的数字呢,当第一个数字选完了,我们回溯回来了,进行i++也就到下一个数字的遍历了,这里的start的作用,同样是告诉for循环本次从哪个数开始选取。而剪枝部分,主要体现在sum>target上,我们当然也可以将它们写在for循环的判断部分,判断sum+nums[i]是否小于等于target这样的话我们才能够进入循环,否则不进入,其实和直接判断思路是一样的。
组合总和2是我们组合回溯的最后一道例题了,它涉及到了去重的逻辑,注意这里只是数组中的单个的数据不能重复选,但是如果数组中给出了有相同的元素我们还是可以选择的它们只是数值上相等,但是所对应的下标不相等,这点很重要。去重的逻辑,我们需要画树型图来辅助理解,我们需要怎么样去重,首先我们应该将数组重新排序,重新排序的目的是让可能重复的数据挨在一起,方便我们去重。当上一个数字和我们本次要取的数字相等时候,可以取吗?答案是可以的,只要是不重复取一个位置就可以
那么什么时候需要去重?当上一个数字和本次要取的数字相等时并且上一个数字我们这个数组中并没有取,但是本次即将要取了,那么我们直接跳过这个数字,为什么呢?因为前面的数字如果和本次数据值相等,那么它们取到的组合是一模一样的,这就造成了答案上的重复,这是一定要避免的,我们可以设立一个数组来保存我们取过的数值,具体看代码
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtacking(vector<int>&candidates,int target,int sum,int startIndex,vector<bool>& used)
{
if(sum==target)
{
result.push_back(path);
return;
}
for(int i=startIndex;i<candidates.size()&&sum+candidates[i]<=target;i++)
{
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false)
{
continue;
}
sum+=candidates[i];
path.push_back(candidates[i]);
used[i]=true;
backtacking(candidates,target,sum,i+1,used);
used[i]=false;
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
path.clear();
result.clear();
sort(candidates.begin(),candidates.end());
backtacking(candidates,target,0,0,used);
return result;
}
};
这里的剪枝逻辑还是和上一题相同的,如果大于了直接跳出,不能再让他搜索到最后才跳出。如果能够理解用数组来标记原数组中的数据是否在本答案数组中被取,和理解为什么我们在遇到两个数挨着相等并且当上一个相等的数没有取这次又取得了我们直接跳出的逻辑,相信这道题还是和不成问题的,这相当于横向的一个答案去重,相等的数字答案组合一定相等的,所以我们才直接continue,让i++遍历其他的数字,但是我们不能够直接break,因为直接break的话我们就无法在本次遍历其他的数字了,遇到了和上一次一样的数字直接跳出来了,会影响后面的数字选出答案。
这是我们回溯算法专题的第一道分割问题,分割问题大多都是较难的,这道题其实也很难。给我们一个字符串,要求是返回它的所有分割方法,且这些分割方法分割出来的子串均为回文子串。
首先要知道怎么分割,可以画树形图来理解,例如分割aab这个字符串,可以先分割a,再分割a再分割b,或者先分割a再将ab做整体分割等等。知道了怎么分割我们再来想回溯函数的终止条件是什么?终止条件实际上是当前的分割线走到了要分割的该字符串的最后一个位置。这是和我们之前做的组合问题的终止条件不一样的地方,一样的是我们可以类比组合问题,这道题仍然需要一个start来标识下一次从哪里进行分割,因为已经被分割的字符串不能重复分割,所以我们要记录下一次分割的位置。那么代码实现,什么来表示分割线呢,就是start。怎么判断子串是否回文呢,这一点很好写,那怎么表示分割子串的区间呢?这一点需要想清楚,start代表分割子串的左区间,i也就是for循环里面的起始遍历位置i代表右区间,start和i不是相等的吗?确实是相等的,但是它们不可能一直相等,循环调整部分i会自增,但是start在本次循环内部不会自增,我们可以调整i来实现对于子串的范围圈定。
class Solution {
public:
vector<vector<string>>result;
vector<string>path;
void backtraking(string s,int start)
{
if(start==s.size())
{
result.push_back(path);return;
}
for(int i=start;i<s.size();i++)
{
if(panduan(s,start,i))
{
string p=s.substr(start,i-start+1);
path.push_back(p);
}
else continue;
backtraking(s,i+1);
path.pop_back();
}
}
bool panduan(string s,int start,int i)
{
int left=start;int right=i;
while(left<right)
{
if(s[left++]!=s[right--])return false;
}
return true;
}
vector<vector<string>> partition(string s)
{
if(s.size()==0)return result;
backtraking(s,0);
return result;
}
};
我们在for循环里判定,如果当前的子串不是回文的,那么我们continue直接让i++增大搜索区间。这里可能有一个疑问的点,那如果搜索的区间永远都是不回文的是不是就搜不到正确答案了,这点不用担心,单个字符就是回文的,所以它一定会往下搜索出一个答案出来。回文的判断代码,十分简单,就只是用双指针将传入的子串范围从前后一起遍历,如果有不等的字符直接return就可以了。
本题的难点在于:分割子串时候如何表示切割字符串的切割线,如何表示切割出来的子串的范围,以及如何写回溯算法中终止条件的代码,如果能够搞清楚,就可以做出这道题目了。
今天我们完成了组合总和、组合总和 II、分割回文串三道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~