题目1:39组合总和
题目链接:组合总和
对题目的理解
整数数组无重复元素,找出数组中数字的和等于目标数target的不同组合,以列表形式返回,
数组中同一个数字可以无限制重复被选取,保证不同组合数少于150个。
数组中的元素在[2.40]之间,至少包含一个元素, target在[1.40]
本题抽象成树形结果如图
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
回溯法
回溯三部曲
i)递归函数参数
定义两个全局变量,二维数组result存放全部结果集,数组path存放单个符合条件的结果。
题目中给出的参数,集合candidates, 和目标值target,还定义了int型的sum变量来统计单一结果path里的总和,还需要startIndex来控制for循环的起始位置
ii)递归终止条件
sum大于target就return,sum等于target的时候,需要收集结果
iii)单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合
代码的详细流程
代码(缺点:对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。)
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int sum, int targetsum, int startindex, vector<int>& candidates){
//终止条件
if(sum>targetsum) return;
if(sum==targetsum){
result.push_back(path);
return;
}
//单层递归逻辑
for(int i=startindex;i<candidates.size();i++){
path.push_back(candidates[i]);
sum+=candidates[i];
backtracking(sum,targetsum,i,candidates);//递归。这里一定要是i,不能是startindex,代表这个for循环依次向下进行
sum-=candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
path.clear();
result.clear();
backtracking(0, target, 0, candidates);
return result;//整数数组至少包含一个元素,所以不用考虑数组为空的情况
}
};
!!!注意:代码中for循环中的递归语句如果是如下语句,那么最终对应的示例结果中会出现重复的元素组合,这是为什么呢???
backtracking(sum,targetsum,startindex,candidates);
绘制了一下流程图,可以很清晰地说明,在图最下方蓝框的部分,出现的那个startindex始终等于0,导致后面的元素又和前面已经遍历过的元素去比较,所以会出错
剪枝优化
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历
在for循环上做文章,在求和问题中,排序之后加剪枝是常见的套路!
代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int sum, int targetsum, int startindex, vector<int>& candidates){
//终止条件
if(sum==targetsum){
result.push_back(path);
return;
}
//单层递归逻辑
for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
path.push_back(candidates[i]);
sum+=candidates[i];
backtracking(sum,targetsum,i,candidates);//递归。这里一定要是i,不能是startindex,代表这个for循环依次向下进行
sum-=candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
path.clear();
result.clear();
sort(candidates.begin(), candidates.end());
backtracking(0, target, 0, candidates);
return result;//整数数组至少包含一个元素,所以不用考虑数组为空的情况
}
};
- 时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
- 空间复杂度: O(target)
题目2:40组合总和Ⅱ
题目链接:组合总和
对题目的理解
求解整数数组中数字和等于目标数targets的组合,每个数字只能使用1次,所有组合不能重复。数组中至少包含1个元素,并且数组中的元素大于等于1。
注意数组中可能存在重复的元素哦,所以数组中可能出现重复的组合,所以需要去重
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合。
如果把所有组合求出来,再用set或者map去重,这么做很容易超时!所以要在搜索的过程中就去掉重复组合。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
Q:是要同一树层上使用过,还是同一树枝上使用过呢?
A:元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重,树层去重的话,需要对数组排序!
回溯法
回溯三部曲
i)递归函数参数
bool型数组used,用来记录同一树枝上的元素是否使用过,这个集合去重就是used来完成的。
ii)递归终止条件
终止条件为 sum > target
和 sum == target
。
iii)单层搜索的逻辑
要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1],此时for循环里就应该做continue的操作。
将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
因为同一树层,used[i - 1] == false 才能表示当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的,而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:
代码流程
代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex, vector<bool>& used){
//终止条件
if(sum>targetsum) return;
if(sum==targetsum){
result.push_back(path);
return;
}
//单层遍历逻辑
for(int i=startindex;i<candidates.size();i++){
//去重
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
path.push_back(candidates[i]);
sum+=candidates[i];
used[i]=true;
backtracking(sum,targetsum,candidates,i+1,used);//递归
used[i]=false;
sum-=candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear();
result.clear();
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
backtracking(0, target, candidates,0,used);
return result;
}
};
剪枝(在for循环上做文章)
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex, vector<bool>& used){
//终止条件
//if(sum>targetsum) return;
if(sum==targetsum){
result.push_back(path);
return;
}
//单层遍历逻辑
for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
//去重
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
path.push_back(candidates[i]);
sum+=candidates[i];
used[i]=true;
backtracking(sum,targetsum,candidates,i+1,used);//递归
used[i]=false;
sum-=candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear();
result.clear();
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
backtracking(0, target, candidates,0,used);
return result;
}
};
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
使用startindex去重(很巧妙的一种思想)
去重的程序流程
代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex){
//终止条件
//if(sum>targetsum) return;
if(sum==targetsum){
result.push_back(path);
return;
}
//单层遍历逻辑
for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
//去重
// 要对同一树层使用过的元素进行跳过
if(i>0 && i>startindex && candidates[i]==candidates[i-1]) continue;
path.push_back(candidates[i]);
sum+=candidates[i];
backtracking(sum,targetsum,candidates,i+1);//递归
sum-=candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear();
result.clear();
sort(candidates.begin(), candidates.end());
backtracking(0, target, candidates,0);
return result;
}
};
题目3:131分割回文串
题目链接:分割回文串
对题目的理解
将字符串s分割成回文串(正读反读相同)。列出所有的分割结果,字符串长度大于等于1
绘制树形结构图
切割问题类似于组合问题,也可以抽象为1个树形结构,如图
递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法
判断回文串
可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。
回溯法
回溯三部曲
i)递归函数参数
全局变量数组path存放切割后单个回文的子串,二维数组result存放所有结果集。
递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题保持一致的。
ii)递归函数终止条件
切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
iii)单层搜索的逻辑
Q:在递归循环中如何截取子串呢?
在for (int i = startIndex; i < s.size(); i++)
循环中定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在vector<string> path
中
注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1
代码逻辑
代码
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){
//终止条件
if(startindex==s.size()){
result.push_back(path);
return;
}
//单层遍历逻辑
for(int i=startindex;i<s.size();i++){
if(ispalidrome(s,startindex,i)){
string str = s.substr(startindex, i-startindex+1);//使用substr提取子串,第一个数代表起始位置,第二个是代表提取子串的长度
path.push_back(str);
}
else continue;
backtracking(s,i+1);//递归
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
path.clear();
result.clear();
backtracking(s,0);
return result;
}
};
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n^2)
本题难点
- 切割问题可以抽象为组合问题
- 如何模拟那些切割线,使用startindex模拟切割线,注意切割过的地方不能重复切割
- 切割问题中递归如何终止,startindex切割到最后一个字符串之后
- !!!!在递归循环中如何截取子串 子串就是[startindex, i]
- 如何判断回文,使用双指针法,同时从首尾向中间移动,比较字符是否相等