LeetCode39.组合总和
题目描述:
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates =[2,3,6,7],
target =7
输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
示例 2:
输入: candidates = [2,3,5],
target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2],
target = 1
输出: []
解题思路:
·这题的解题思路与之前的题目是一致的,是需要注意的是,这题含有重复元素这一定义,所以,再遍历的过程中,startIndex不需要想后移动
代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(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]);
backtracking(candidates,target,sum,i);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if(candidates.size() == 0) return result;
backtracking(candidates,target,0,0);
return result;
}
};
·时间复杂度:O(n*2^n),注意这里只是时间复杂度的上界
·空间复杂度:O(target)
总结:这题与之前的题目有两点不同1.组合没有数量要求2.元素可无限重复选取
本题中使用startIndex来控制for循环的起始位置,对于组合问题中:
如果一个集合来求组合的话,就需要startIndex,例如之前写的77.组合,216.组合总和III
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如之前的17.电话号码的字母组合
LeetCode40.组合总和II
题目描述:
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates =[10,1,2,7,6,1,5]
, target =8
, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]
解题思路:
·本题的难点在于,集合中可以有重复元素,但是不能有重复的组合,有些同学的想法是使用map或者set进行去重,但是这样使用并不能通过,所以我们要在搜索过程中去掉重复组合
·有很多同学不理解这里去重的意思,可以这样理解,就是使用过的元素不能重复使用。说起来是很简单的,但是如果抽象成树结构那么就不好思考出了,因为在树结构上,存在两个维度,一个维度是同一树杈上使用过,一个维度是同一树层次上使用过。
·又有新的问题了,那么到底是选择同一树层上的,还是同一树杈上的,题目中说了,可以有重复的元素,但是不能有重复的组合,就可以列一个简单的例子,自己将树结构抽象画图出来,就明白了,如图:
·所以,我们要去重的是同一数层上使用过的,同一树枝上的都是一个组合内的元素,不用去重。
*特别强调,在对数组操作前,要将数组内元素进行排序
·我们使用一个bool型数组used,用于记录同一树枝上的元素是否使用过,若为true则未使用过,反之则使用过,因为已经将元素排序,所以当candidates[i] == candidates[i-1]并且used[i-1] == false,就说明前一个树枝使用了candidates[i-1],也就是说同一数层使用过candidates[i-1],则跳出循环,进行下一次遍历
*文字描述比较抽象,配合图片一起食用
代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(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()&&candidates[i]+sum <= 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;
backtracking(candidates,target,sum,i+1,used);
used[i] = false;
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0,used);
return result;
}
};
·时间复杂度:O(n*2^n)
·空间复杂度:O(n)
难点/易错点
·对元素去重的理解,是去除重复元素,还是去除重复组合
·没有对数组元素进行排序
·不知道如何确定元素是否被使用过
·停止搜索条件、递归条件、重复元素的定义
总结:因为这道题有去重这一步骤,所以这道题比之前做的回溯的题目更加的困难,关键是去重的逻辑,代码并不难理解,但是要把代码含义理解明白,这道题比较偏向逻辑,虽然是考察逻辑性,但是依旧是使用的回溯的基本模板。
LeetCode131.分割回文串
题目描述:
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a" 输出:[["a"]]
解题思路:
·切割问题类似组合问题
例如字符串abcdef:
1.组合问题:选取一个a之后,再bcdef中再去选取第二个,选取b之后再cdef中再选取第三个...
2.切割问题:切割一个a之后,再bcdef中再去切割第二段,切割b之后在cdef中再切割第三段...
·所以我们就可以将切割问题抽象成一棵树形结构:
·递归用来纵向遍历,for循环用来横向遍历,切割线(图中红线)切割到字符串的结尾位置,说明找到了一个切割方法。所以我们就可以发现,切割问题的回溯搜索的过程和则核问题的回溯搜索的过程是差不多的。
代码如下:
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
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(huiwen(s,startIndex,i)){
string str = s.substr(startIndex,i-startIndex+1);
path.push_back(str);
}else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
}
bool huiwen(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;
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return result;
}
};
·时间复杂度:O(n*2^n)
·空间复杂度:O(n^2)
难点:
·切割问题可以抽象成组合问题
·如何模拟哪些切割线
·切割问题总递归如何终止
·在递归循环中如何截取子串
·如何判断回文
总结:这道题可以说啥比较有难度了,有很多同学包括我自己,遇到题目知道比较难,但是不知道题目难在哪里。但是这样说明思维不够清晰。
之前在说明回溯法基础的时候说明了回溯法可以解决切割问题,但是在做这题的时候第一个i难点就是:不知道如何切割,甚至也不知道在哪里需要使用回溯法。也就是没有体会到按照组合问题的套路就可以解决切割。
但是接下来如何模拟切割线,如何终止,如何截取子串,都不好想,可以说是判断回文是最简单的了。
并且,需要搞明白,关于模拟切割线,其实就是index是上一层已经确定了的分割线,i是这一层试图寻找的新分割线。搞懂了这些,这道题其实也就没有那么难了。