39.组合总和
本题一定要注意题干:候选元素都是正数,不然很难确定终止条件
抽象问题为树形结构如下
我们记录的还是边,选用记录边的回溯模板
模板选取好了,我们需要确定辅助用的全局变量和终止条件。这里定义三个全局变量,二维数组result存放满足条件的结果集,数组path用于在遍历中记录取到的组合,一个sum用于在遍历中记录组合总和
vector<int> path;
vector<vector<int> > result;
int sum = 0;
终止条件是什么?因为候选元素都是正数,当sum大于target了,往后面继续走就没有意义了,此时需要往回走。当sum等于target,说明找到了一个可行的组合,此时需要做记录,然后往回走去寻找别的可行的组合
void backtracking(vector<int> & candidates, int target) {
if (sum > target) // 终止条件,往回走
return;
if (sum == target) { // 终止条件,添加记录并往回走
result.push_back(path);
return;
}
for (路径:所有可选择的路径) {
添加记录;
backtracking(路径,其他参数); // 前往下一个节点
撤销记录; // 回溯
}
return; // 往回走
}
如果没有到达终止条件,则跨过 if 语句,此时我们就需要考虑:站在当前节点,有哪些路径可以去?(即有哪些元素可以取)从题意我们看出,站在一个节点有哪些元素可以取和回溯函数走到这个节点所取到的元素有关,因此,我们用参数startIndex表示走到当前节点取到的元素下标,不同的startIndex表示在该节点正在处理的不同元素,也标识了不同的节点及不同的后续可取元素(不同的可前往的路径)
知道了startIndex,自然就能知道有在当前节点有哪些可以选择的路径,进而做出选择从而继续前往下一个节点
如何前往下一个节点?看图
整体代码
class Solution {
private:
vector<int> path;
vector<vector<int> > result;
int sum = 0;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);
return result;
}
void backtracking(vector<int> & candidates, int target, int startIndex) {
if (sum > target)
return;
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); ++i) {
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, i); // 选择路径i,前往下一个节点
path.pop_back();
sum -= candidates[i]; // 回溯需要维护path和sum
}
}
};
注意添加记录和撤销记录时,需要同时维护path和sum
40.组合总和II
本题和之前的最大区别:候选集合中含有重复元素!
候选集合有重复元素说明,当我们站在一个节点,准备选择路径前往后续节点时,有重复的路径
因此我们要在选择路径时,判断该路径是否之前选择过,如果选择过的话,需要跳过
很容易在之前代码上修改得到下面的代码,核心思想是记录在每个节点时,途径该节点已经去过的路径,免得重复选择相同路径
class Solution {
private:
vector<int> path;
vector<vector<int> > result;
int sum = 0;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);
return result;
}
void backtracking(vector<int> & candidates, int target, int startIndex) {
if (sum > target) // 终止条件,返回
return;
if (sum == target) { // 终止条件,记录结果并返回
result.push_back(path);
return;
}
unordered_set<int> used; // 用于存储在该节点已经选择过的路径
for (int i = startIndex; i < candidates.size(); ++i) {
if (used.find(candidates[i]) != used.end()) // 如果已经选择过这条路径,则跳过
continue;
used.insert(candidates[i]); // 否则置这条路径为“已选择”,并前往该路径
path.push_back(candidates[i]); // 添加记录并同时维护path和sum
sum += candidates[i];
backtracking(candidates, target, i + 1); // 传入i+1是因为每个索引对应的数字只能使用一次
path.pop_back(); // 撤销记录并同时维护path和sum
sum -= candidates[i];
}
return; // 返回
}
};
看似没啥错误对吧,就是剪去了相同路径。可是为什么会报错呢?
原因如图:
问题就在,我们选择路径的时候,比如我们一开始选了1到达了一个节点,然后后面可选路径有7;接下来我们会选择了7到达另一个节点,后面可选的路径还有1,于是就有重复组合了
这里的“后面” 指的是索引更大的位置
我们的目标是想选择7之后,后面可选路径不再有1(即比值7的索引更大的索引位置上不再有1)
简单,先对 candidates 排个序就行!
最终代码
class Solution {
private:
vector<int> path;
vector<vector<int> > result;
int sum = 0;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 排个序就好!
backtracking(candidates, target, 0);
return result;
}
void backtracking(vector<int> & candidates, int target, int startIndex) {
if (sum > target)
return;
if (sum == target) {
result.push_back(path);
return;
}
unordered_set<int> used;
for (int i = startIndex; i < candidates.size(); ++i) {
if (used.find(candidates[i]) != used.end())
continue;
used.insert(candidates[i]);
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, i + 1);
path.pop_back();
sum -= candidates[i];
}
return;
}
};
131.分割回文串
本题最大的难点就是抽象为树形结构
例如对于字符串abcdef:
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....
本题也是需要记录每次的截取方案,即需要记录路径,采用记录边的回溯模板
模板选取好了,我们需要确定辅助用的全局变量和终止条件。这里定义两个全局变量,二维数组result存放满足条件的截取方案,数组path用于切割中记录截取到的已是回文的子串
vector<vector<string> > result;
vector<string> path; // 放已经回文的子串
接下来判定终止条件:从上图可以看出,我们的字符串是从左向右截取的。当剩余可截取的字符串为空时,则没办法继续截取子串了,应该终止
if (s.size() == 0) {
result.push_back(path);
return;
}
注意我们的path存的已经是回文的子串,当剩余可截取的字符串为空时,说明我们的将整个字符串都截取成了不同的回文子串,因此到这个叶子节点时,标记着一套成功的截取方案,应该加入结果集
如果剩余还有可截取的字符串,则应该跨过if语句,进行进一步的截取。即又回到之前题目中类似的问题:我们在这个节点,应该怎么走
分析一下:怎么走,换句话说就是怎么截取,要想知道怎么截取,就应该知道剩下可截取的字符串。不同的剩余可截取字符串决定了下一步怎么截取,不同的剩余可截取字符串也标识了不同节点
因此,我们的回溯函数传入剩余可截取字符串
void backtracking(const string & s)
然后,我们开始执行截取(即开始选择路径):依次截取可截取字符串的首个字符、前两个字符、前三个字符......作为子串,直到整个可截取字符串全部被截取。每次截取得到的子串需要判断这个子串是否回文串,如果是回文串才需要继续截取,如果不是回文串,说明这套截取子串的方案必然不可能加入result,没必要继续截下去了,直接continue
for (int i = 1; i <= s.size(); ++i) { // 依次截取字符串前1、前2、...、前size个字符
string sub = s.substr(0, i); // 截取到的子串
if (isPalindrome(sub)) { // 是回文串才需要继续截取
// 做记录
// 前往下一个节点
// 撤销记录
} else
continue;
}
如果截取到的子串是回文串,我们就需要记录这次截取(即记录下这条路径),然后前往下一个节点。如何前往?之前说了不同的剩余可截取字符串标识了不同的节点, 只需要明确这次截取后,剩余的可截取字符串是什么,传入回溯函数,就表示回溯函数前往了下一个节点进行进一步的截取
完整代码
class Solution {
private:
vector<string> path;
vector<vector<string> > result;
public:
vector<vector<string>> partition(string s) {
backtracking(s);
return result;
}
void backtracking(const string & s) {
if (s.size() == 0) {
result.push_back(path);
return;
}
// 开始选择路径
for (int i = 1; i <= s.size(); ++i) { // 依次截取前1个字符、前2个字符、...、前size个字符
string sub = s.substr(0, i);
if (isPalindrome(sub)) {
path.push_back(sub);
backtracking(s.substr(i));
path.pop_back();
} else
continue;
}
}
bool isPalindrome(const string & s) { // 判断字符串是否回文串
int left = 0, right = s.size() - 1; // 双指针分别指向首尾字符
while (left < right) {
if (s[left] != s[right]) return false;
++left;
--right;
}
return true;
}
};
回顾总结
如何寻找在某个节点可以去的路径?一种思路方法:能否通过设定传入参数,依据传入参数推导出在该节点可以去的路径?如果可以的话,这个参数就能够标识不同的节点,在回溯函数中改变该参数调用回溯函数本身也能够表明“前往了下一个节点”
涉及去重相关问题需要好好演绎思考