39. 组合总和
一、做题感受&第一想法
-
一开始犯的错误:
没有设置startIndex。导致得到了重复的组合!
虽然题目允许重复数字,但是为了不得到重复的组合,还是需要限制每次for循环的startIndex! -
通过代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int sum = 0;
backtracking(target,candidates,sum,0);
return result;
}
void backtracking(int target, vector<int> candidates, int sum, int startIndex){
if(sum == target){
result.push_back(path);
return;
}
else if(sum > target){
return;
}
for(int i = startIndex;i < candidates.size();i++){
path.push_back(candidates[i]);
backtracking(target,candidates,sum + candidates[i],i);
path.pop_back();
}
return;
}
};
二、学习文章后收获
1.startIndex
什么时候需要用?
- 如果是一个集合来求组合的话,就需要startIndex
- 元素可重复,
startIndex = i
- 元素不可重复,
startIndex = i + 1
- 元素可重复,
- 如果是多个集合求组合的话,不需要。
2.剪枝
如果当前的sum加上这层for循环会加上的candidate[i]
,已经大于target,则该层循环直接跳过。
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
...
}
3.复杂度
时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
空间复杂度: O(target)
why???
40. 组合总和 II
一、做题感受&第一想法
不知道怎么去重。
犯了经典错误,确实达成了一个元素只用一次,但是出现了重复的集合。
二、学习文章后收获
1两种去重
- 树层去重:树层(宽度)维度上的去重。本题中,排序后,相同元素只需回溯搜索第一个,因为后面都是重复的。
- 树枝去重:树枝(深度)维度上的去重。本题中,相同的元素也可以出现在同一集合里,所以不需要树枝方向的去重。
2.回溯三要素分析
- 函数参数:candidates,target,startIndex(因为是同一集合内的组合,需要表明for循环的开始位置),used数组(用于记录每个元素的使用情况),sum
- 返回条件:sum == target(收集结果)或sum > target(直接返回)
- 单层回溯的逻辑:for循环中,如果该元素与上个元素相同,而且上个元素未使用,则跳过当前元素(即剪枝去重);否则,正常回溯。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
bool* used = new bool[candidates.size()];
for(int i = 0;i < candidates.size();i++){
used[i] = false;
}
backtracking(candidates,target,0,used,0);
return result;
}
void backtracking(vector<int> candidates, int target, int startIndex, bool* used,int sum){
if(sum == target){
result.push_back(path);
return;
}
else if(sum > target){
return;
}
for(int i = startIndex;i < candidates.size();i++){
//如果该元素与上个元素相同,而且上个元素未使用,则跳过当前元素,即剪枝去重
if(i >= 1 && candidates[i] == candidates[i-1] && used[i-1] == false){
continue;
}
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates,target,i+1,used,sum+candidates[i]);
path.pop_back();
used[i] = false;
}
return;
}
};
请你再读一遍文章~没读完
131.分割回文串
一、做题感受&第一想法
一开始没有思路,因为不知道怎么”有规律“地分割。之后看了文章里的图,明白了分割的规律在于for循环中逐步增加字串的大小。之后写出来了,通过。
回溯三要素的分析
- 函数参数:字符串s,后续分割的起始位置(也就是剩下的字符串的起始位置)startIndex
- 返回条件:startIndex == s.size(),说明整个字符串分割完了。
- 处理回溯的逻辑:for循环中,逐步增大本层所分割子串的长度。如果是回文串,则加入path并进入下一层回溯。否则continue。
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
bool huiwen(string sub){
int i = 0, j = sub.size() - 1;
while(i<j){
if(sub[i] != sub[j]) return false;
i++;
j--;
}
return true;
}
vector<vector<string>> partition(string s) {
if(s.size()) backtracking(s,0);
return result;
}
void backtracking(string s,int startIndex){
if(startIndex == s.size()){
result.push_back(path);
return;
}
//从startIndex开始,把后序字符串依次尝试分割为长度为1,2,...s.size()-startIndex+1的字串
for(int i = startIndex;i < s.size();i++){ //i用于记录字串的结尾位置
string sub = s.substr(startIndex,i - startIndex + 1); //下标为startIndex-i的子串
if(!huiwen(sub)) continue;
path.push_back(sub);
backtracking(s,i + 1);
path.pop_back();
}
return;
}
};
关于判断是否回文串:(回文 palindrome)
bool isPalindrome(string sub){
int i = 0, j = sub.size() - 1;
while(i<j){
if(sub[i] != sub[j]) return false;
i++;
j--;
}
return true;
}
二、学习文章后收获
1.复杂度
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n^2)
2.对比“分割”问题和“组合”问题
- 最大的不同,从每层回溯的子集上考虑:
- 组合:子集中是单个元素, 比如{2,3,4},用for循环遍历这些元素
- 分割:子集中是来源于同一字符串、长度为1~k的子串,它们起始于同一位置,但长度各不相同, 比如从”aabcd“可以得到{”a“,“aa”,“aab”,“aabc”,“aabcd”}。for循环控制子串长度,从而生成这些子串,遍历这些情况。
3.判断回文串的优化(动态规划,后续补上)
三、过程中遇到的问题
1.string常用的操作
# include<string>
string str;
//子串
str.substr(pos,npos) //取子串,开始位置pos,长度npos
//插入
str.insert(pos,c) //开始位置pos,插入一个字符c
str.insert(pos,s) //开始位置pos,插入一个字符串s
str.insert(pos,n,c) //开始位置pos,插入n个字符c
//删除
str.erase(pos,npos) //删除一部分子串,开始位置pos,长度npos
//查找
str.find(s) //查找s这个子串第一次出现的位置,找不到返回-1
str.find(s,pos = 0) //从pos位置开始(默认0),查找s这个子串第一次出现的位置
str.find(c,pos = 0) //从pos位置开始(默认0),查找c这个字符第一次出现的位置
str.find(s,pos,n) //从pos位置开始(默认0),查找s这个字符串串的前n个字符第一次出现的位置
//右查找
str.rfind(s)
str.rfind(s,pos = 0)
str.rfind(c,pos = 0)
str.rfind(s,pos,n)
//替换
str.replace(pos,n,str) //从pos开始的n个字符替换成str
//字符串比较
str.compare(s) //比较str和s。字符串比较是按字符的ASCII码进行对比,返回0则相等!