题目:39.组合总和
读完题感觉不是那么难?与77.组合的区别不就是每层递归for循环所遍历的范围么?本题是不用去重的。
也就是说这道题深度未知
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
int sum = 0;
void backtracking(vector<int>& candidates, int index, int target){
if(sum == target){
result.push_back(path);
}
if(index == 4) return; //这个终止条件是有问题的。谁说的最多只能放四个元素在path里?
for(int i = 0; i < candidates.size(); i++){
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, index + 1, target);
path.pop_back();
sum -= candidates[i];
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if(candidates.size() == 0) return result;
backtracking(candidates, 0, target);
return result;
}
};
写出了这样的代码,错了,因为例如[2,3,2][2,2,3]重复统计
怎样避免统计重复的情况呢?
其实这道题的数据本身就帮我们省了很多事:不含0元素,无重复元素(不用去重)
本题正确的终止条件应该是:sum > target
那么index在这道题里有什么用呢?
以上这两个问题,归结于一个解决方法(也是纠正错误),那就是本题还是需要startIndex而是不是index。
首先辨析一下本题和77.组合,对于”不重复“这一要求有什么不同。77.组合要求不同的path不能是同样的元素组成,每一个path里也不能出现相同的元素。而这一道题只是要求不同的path不能是同样的元素组成,但在一个path里可以出现相同的数。
深刻理解这一点,就可以找到避免两个path相同的办法,那就是设置startIndex,规定下一个for循环从哪里开始搜索。
修改后的代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
int sum = 0;
void backtracking(vector<int>& candidates, int startIndex, int target){
if(sum == target){
result.push_back(path);
}
if(sum > target) return;
for(int i = startIndex; i < candidates.size(); i++){
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, i, target); //不用i + 1,表示可以去与当前重复的数
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if(candidates.size() == 0) return result;
backtracking(candidates, 0, target);
return result;
}
};
剪枝的操作:
对数组先排序,当有一个组合总和sum大于target了,那么他后面的元素也没有必要再进行递归了。
剪枝代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
int sum = 0;
void backtracking(vector<int>& candidates, int startIndex, int target){
if(sum == target){
result.push_back(path);
}
if(sum > target) return;
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){ //如果不满足,提前终止for循环,实现剪枝
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, i, target); //不用i + 1,表示可以去与当前重复的数
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
if(candidates.size() == 0) return result;
backtracking(candidates, 0, target);
return result;
}
};
题目:40.组合总和||
读完题一阵兴奋,在深刻理解77.组合和39.组合总和||的联系与差别之后,这道题可以说是信手拈来了,与77.组合相比,则不规定每一条path的元素个数,与39.组合总和||相比,每一条path里可以出现相同的元素。
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){
if(i > 0 && candidates[i] == candidates[i - 1]) continue;
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, i + 1, target, sum); //传入i + 1而不是i,不能取与当前相同的数
path.pop_back();
sum -= candidates[i];
}
但这样是不对的,因为这样的话在下一层里会直接跳过1了,漏掉了[1,1,6]这种情况。
怎么解决?
猜测:如果要改的话应该将去重写在for循环里,而不能在循环体内,应该怎么写呢?(该猜测是错的)
这道题涉及到了回溯中一个重要的操作:去重
去重分两种:数层去重和树枝去重。本题需要做的是树层去重
第一种改法(来自b站评论):
将 if(i > 0 && candidates[i] == candidates[i - 1]) continue;
改为if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
修改完的代码,可以保证每一层的递归for循环里,第一个遍历到的元素candidates[startIndex]肯定会被用一次,避免了因为candidates[startIndex] = candidates[startIndex - 1]而被忽略的情况,也就是说candidates[startIndex]至少会被用一次。
这样写统一了每一层for循环里的if判断。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int startIndex, int target, int sum){
if(sum == target){ //收获结果
result.push_back(path);
return;
}
if(sum > target){
return;
}
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){
if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, i + 1, target, sum); //传入i + 1而不是i,不能取与当前相同的数
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
if(candidates.size() == 0) return result;
backtracking(candidates, 0, target, 0);
return result;
}
};
第二种改法(来自标答):
借助used数组。
定义bool类型数组used来标记每一个数字被用过没有。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int startIndex, int target, int sum, vector<bool>& used){
if(sum == target){ //收获结果
result.push_back(path);
return;
}
if(sum > target){
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;
}
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
path.push_back(candidates[i]);
sum += candidates[i];
used[i] = true;
backtracking(candidates, i + 1, target, sum, used); //传入i + 1而不是i,不能取与当前相同的数
path.pop_back();
sum -= candidates[i];
used[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
//首先把candidates排序,让其相同的数紧挨在一起
sort(candidates.begin(), candidates.end());
if(candidates.size() == 0) return result;
backtracking(candidates, 0, target, 0, used);
return result;
}
};
比较而言,利用used来标记状态是更普遍的做法。(第一种不一定每次都能想到)
题目:131.分割回文串(有点难理解,主要切割线很陌生)
尝试作答:
正常递归加回溯,每一层的终止条件里判断目前path是否是回文串,如果是则加入result。这样设置终止条件是否略显冗杂?
其实以上想法是错误的,如果还是像以前做过的题那种收集path的话,每次都是用第一个元素开头。本题应该考虑的是选取切割的位置。
那么,代码怎么来表示切割呢?
传入的startIndex就是切割线。本层字串的范围为startIndex到i。在单层递归逻辑里进行是否满徐回文的判断。
substr是C++语言函数,主要功能是复制子字符串,要求从指定位置开始,并具有指定的长度。
substr的两个参数分别是:起始位置和长度
class Solution {
public:
vector<string> path;
vector<vector<string>> result;
bool isPalidrome(const string& s, int start, int end){
for(int i = start, j = end; i < j; i++, j--){
if(s[i] != s[j]) return false;
}
return true;
}
void backtracking(const string& s, int startIndex){
// 如果起始位置已经等于s的大小,说明已经找到了一组分割方案了
if(startIndex == s.size()){
result.push_back(path);
}
for(int i = startIndex; i < s.size(); i++){
if(isPalidrome(s, startIndex, i)){ //这一坨传参数好绕,主要想不明白切割线的位置
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
//path里装了本条切割路径得到的所有子串
//if判断放在循环体内,保证了加入path的所有子串必是回文的
}
else{
continue;
}
backtracking(s, i + 1);
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return result;
}
};
终止条件有点难理解
传参很绕:传startIndex还是startIndex + 1? 传 i 还是 i + 1?
本题难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文