前置知识
参考前文
参考文章:
LeetCode刷题笔记【18】:回溯专题-1(回溯算法基础知识、用回溯算法解决组合问题)
LeetCode刷题笔记【19】:回溯专题-2(组合总和III、电话号码的字母组合)
39. 组合总和
题目描述
经典回溯
思路: 使用回溯
注意循环时从index
开始, 而不是index+1
开始, 因为题目中允许同一个数字反复选取
可以用判断sum
是否>target
来剪枝(如果candidates
里面都为正数的话)
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
int sum;
public:
void backtrack(int index, vector<int>& candidates, int target){
if(sum>target) return;
if(sum==target){
ans.push_back(path);
return;
}
for(int i=index; i<candidates.size(); ++i){
path.push_back(candidates[i]);
sum += candidates[i];
backtrack(i, candidates, target);
path.pop_back();
sum -= candidates[i];
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
ans.clear();
path.clear();
sum=0;
backtrack(0, candidates, target);
return ans;
}
};
改进思路
进一步优化:不在递归函数开头判断sum>target
, 而在循环体内判断
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
int sum;
public:
void backtrack(int index, vector<int>& candidates, int target){
if(sum==target){
ans.push_back(path);
return;
}
for(int i=index; i<candidates.size() && sum+candidates[i]<=target; ++i){
path.push_back(candidates[i]);
sum += candidates[i];
backtrack(i, candidates, target);
path.pop_back();
sum -= candidates[i];
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
ans.clear();
path.clear();
sum=0;
sort(candidates.begin(), candidates.end());
backtrack(0, candidates, target);
return ans;
}
};
40.组合总和II
题目描述
LeetCode链接:https://leetcode.cn/problems/combination-sum-ii/description/
解题思路
在**<39. 组合总和>**的基础上, 还要求过程中candidates
中每个数字在每个组合中只能使用一次。
那么就先sort
排序, 然后在循环过程中加一个检测: 前后相同则跳过;
为什么这样可以达成去重的目的?
我们以本题举例:
输入: candidates = [10,1,2,7,6,1,5], target = 8
如果不加这两行, 那么输出结果是: [[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]]
注意多次输出了[1,2,5],[1,7], 这就是多次使用了第二个1
如果加上重复跳过, 则在到第二个1时, 其会直接跳过, 并不会重复;
那么问题来了, 是否会跳过[1,1,6]呢?
其实不会, 因为是i>index
才会跳过, [1,1,6]从第一个1开始, 到第二个1时index=1
, 所以i=1的时候虽然 candidates[i]==candidates[i-1]
, 但是不会跳过.
代码
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
int sum;
void backtrack(int index, vector<int>& candidates, int target){
if(sum>target) return;
if(sum==target){
ans.push_back(path);
return;
}
for(int i=index; i<candidates.size(); ++i){
if(i>index && candidates[i]==candidates[i-1])//关键, 十分关键
continue;
path.push_back(candidates[i]);
sum += candidates[i];
backtrack(i+1, candidates, target);
path.pop_back();
sum -= candidates[i];
}
return;
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
ans.clear();
path.clear();
sum=0;
sort(candidates.begin(), candidates.end());
backtrack(0, candidates, target);
return ans;
}
};
131.分割回文串
题目描述
LeetCode链接:https://leetcode.cn/problems/palindrome-partitioning/description/
解题思路
思路: 两个辅助函数, 一个bactrck
用于实现回溯, 一个check
用于检查子串是否是回文串
backtrack
中, 传入left
, 依次尝试不同的right
, 即不同长度的当前子串, 一直尝试到总串结尾
代码
class Solution {
private:
vector<vector<string>> ans;
vector<string> path;
bool check(int left, int right, const string& s){
while(left<=right){
if(s[left] != s[right])
return false;
left++;
right--;
}
return true;
}
void backtrack(int left, const string& s){
// cout << "left= " << left << endl;
if(left>=s.size()){
ans.push_back(path);
return;
}
for(int right=left; right<s.size(); ++right){
// cout << "right= " << right << endl;
if(check(left, right, s)){
// cout << "checking success" << endl;
path.push_back(s.substr(left, right-left+1));
backtrack(right+1, s);
path.pop_back();
}
}
}
public:
vector<vector<string>> partition(string s) {
backtrack(0, s);
return ans;
}
};
总结
在不同的回溯算法使用方式中, 今天的前两个题很好的比较了"允许重复"和"不允许重复"的情况.
除了昨天总结中提到的:
今天还涉及到了需要排序后跳过重复元素的操作, 需要好好对比学习.
而今天的第三题, 是一个很好的展现"如何将一个陌生问题抽象为熟悉的情景, 并使用熟练的工具去解决"的过程;
本身其并不是一个典型的回溯问题, "回文串"的描述更是具有迷惑性, 但我们还是可以通过双指针法, 将其抽象为对字符串中元素的遍历和回溯.