39. 组合总和
1.回溯
vector<vector<int>> res;
vector<int> path;
int sum;
void backtracing(vector<int>& candidates, int target, int start)
{
if (sum > target) return;
if (sum == target)
{
res.push_back(path);
return;
}
for (int i = start; i < candidates.size(); i++)
{
path.push_back(candidates[i]);
sum += candidates[i];
backtracing(candidates, target, i);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
res.clear();
path.clear();
if (candidates.empty()) return res;
backtracing(candidates, target, 0);
return res;
}
2.剪枝
如果是升序可以剪枝,但这个题目candidates并没有说升序,没法剪掉
40.组合总和 II
这个题目主要问题在去重,如果想用set去重的话是做不到的,会超出内存限制
如果有相同数字的话,前面的情况一定会包含后面的,我们需要把后面的情况剪掉
首先对样例排序。
这里我们以题目中的样例:1,1,2,5,6,7,10
为例子
在下面的图中,可以看到第一个1的后续分支一定会包括了第二个1的后续分支,并且会额外包含用了两个1的情况(注意2nd:2,5,6,7,10
与3rd:2,5,6,7,10
含义是不同的,后者是用了两个1的;这里说的包含指的是2nd:1,2,5,6,7,10
包含了2nd:2,5,6,7,10
,两者都只取了一个1)
那么根据此,我们只需要把2nd:2,5,6,7,10
这个分支剪掉就可以了。
这个剪掉的判别的条件:1.它不能是第一个;2.它和前面的一样(从start算起,可不是从0算起,从0算起上面的3rd:2,5,6,7,10
可就被剪掉了,显然有错误)
vector<vector<int>> res;
vector<int> path;
int sum;
void backtracing(vector<int>& candidates, int target, int start)
{
if (sum > target) return;
if (sum == target)
{
res.push_back(path);
return;
}
for (int i = start; i < candidates.size(); i++)
{
if (i > start && candidates[i] == candidates[i - 1]) continue;
path.push_back(candidates[i]);
sum += candidates[i];
backtracing(candidates, target, i + 1);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
res.clear();
path.clear();
if (candidates.empty()) return res;
sort(candidates.begin(), candidates.end());
backtracing(candidates, target, 0);
return res;
}
2.代码随想录的方法
随想录的补充方法就是我想的这个,他的第一个用used标记的方法,这个方法used就是告诉你这个数字是否已经在path中
如果它用过了,那么你再往里放一样的元素就是合法的,可能的结果例如1,1,6
如果它没用过,那么你放一样的元素就认为是不合法的,就会产生1(first),2,5
和1(second),2,5
这种重复情况。
used可以优化掉,原理就是前面的方法,只需要发现它和之前的可选元素重复就认为不合法。再次强调一下,可选指的是从start开始,不是从0开始。
131.分割回文串
首先,返回全部切割结果就已经是一个问题了。
1.自己写的
我自己的思考路线是这样:
-
1.需要一个判别回文的函数
isReverse()
-
2.需要一个能返回所有切片的回溯。
前者很容易实现,翻转前后是否一致即可判别。
后者相对复杂,因为后者在回溯的树上不只需要叶子结点,每一个结点都可能是结果,我们以abcd
为例
可以看到,当最后一段只有一个字符时,即到达叶子结点,我们可以在回溯前加上字符串时利用isReverse()
阻止掉不合法的切片方式。
但是,别忘了,这个树不是只有叶结点算切片方式,而是所有结点,因此我们在每次开始本轮切片时,需要先把当前的切片方式依照其是否合法纳入结果
vector<vector<string>> res;
vector<string> path;
bool isReverse(string str)
{
string tmp = str;
reverse(tmp.begin(), tmp.end());
return str == tmp;
}
void backtracing(string str)
{
//这里每次都针对上一层的最后一段,如果这一段是回文,那么这种切片合法(前面几段的合法性在上一层判别过,不合法不会进入这一层)
if (isReverse(str))
{
path.push_back(str);
res.push_back(path);
path.pop_back();
}
for (int i = 0; i < str.size() - 1; i++)
{
string str1 = str.substr(0, i + 1);
if (!isReverse(str1)) continue;
path.push_back(str1);
backtracing(str.substr(i + 1, str.size()));
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
res.clear();
path.clear();
if (s.empty()) return res;
backtracing(s);
return res;
}
2.代码随想录的方法
代码随想录的方法是基于原来的模板的思考方式。
我认为还是有必要照这个思路做一遍,因为感觉自己是对回溯不熟,所以写的代码并不是很模板化。
他的逻辑是一定要切到结尾才认为结束,这个思路我认为我应当学习,因为我没想到这一点,而是选择把所有节点归入结果判别,不是很符合回溯的逻辑
这个可能看文字不好理解,我们可以通过类似上面的图来理解。给每一个结点都多加了一个切到尾部的结点,从而使得问题回到那个只需要叶结点的回溯问题
void backtracing(const string& str, int startIndex)
{
if (startIndex >= str.size())
{
res.push_back(path);
return;
}
for (int i = startIndex; i < str.size(); i++)
{
string tmp = str.substr(startIndex, i - startIndex + 1);
if (isReverse(tmp))
{
path.push_back(tmp);
backtracing(str, i + 1);
path.pop_back();
}
else
{
continue;
}
}
}
优化是提前算好是否回文,与回溯关系不大,就不写啦