力扣题目链接/文章讲解/视频讲解
在昨天那道131.分割回文串中,我们是将字符串截取成回文子串,然后记录下截取后得到的回文子串
这道题其实是完全相同的思路。区别只有如下:
我们就照着上一道题的写法来写
class Solution {
private:
vector result; // 用于储存结果
string path; // 用于在遍历过程中记录分割出来的ip段
int part = 0; // 用于记录已经截取到的段数
public:
vector restoreIpAddresses(string s) {
backtracking(s);
return result;
}
void backtracking(const string & s) {
if (part == 3) { // 终止条件:当前面已经分割出三段了,则应该把剩余子串作为第四段
if (isValid(s)) {
path += s; // 如果第四段也是有效段
result.push_back(path); // 记录结果
}
return;
} else if (s.size() == 0) return;
for (int i = 1; i <= s.size() && i <= 3; ++i) {
string sub = s.substr(0, i);
if (isValid(sub)) { // 是有效ip地址段才需要继续截取
string inital = path; // 用于回溯
++part;
path += sub;
path += '.';
backtracking(s.substr(i));
path = inital; // 回溯
--part;
} else
continue;
}
return;
}
bool isValid(const string & s) {
if (s.empty()) return false;
if (s.size() > 1 && s[0] == '0') return false;
double num = atof(s.data()); // atof接收一个const char *,返回一个double,s.data从string对象中返回一个const char *
if (num > 255) return false;
return true;
}
};
本题和上一道几乎相同,回溯函数的参数是“剩余子串”,可用来标记回溯函数走到了不同节点
注意substr的用法
力扣题目链接/文章讲解/视频讲解
本题抽象为树形结构如下
本题和组合问题不一样的地方在于:组合问题只有到达叶子节点才可能收集结果。而由图可知:子集问题到每个节点都需要收集结果
本题的需要记录的结果仍然是路径,因此还是选择记录路径的回溯算法模板
需要全局变量数组path在遍历过程中记录子集元素,二维数组result存放结果
vector path;
vector > result;
终止条件:当剩余集合为空,返回,即当无可选择路径,就终止返回
换句话说就是startIndex已经大于数组的最大索引值时,就终止了,因为没有元素可取了(如果不理解可以往后继续看代码,后面for循环能够显示出startIndex和剩余可取元素的关系)
if (startIndex >= nums.size()) {
return;
}
startIndex能够标识节点:startIndex能够控制在该节点时,可以选择的路径有哪些(剩余的集合有哪几个元素)
我们传入startIndex,很容易就能够知道在该节点,还能往哪些路径前行
如何表示“走向下一个节点”?注意我们的startIndex标记了不同节点,我们向回溯函数按题目需求传入不同的startIndex, 就能够表明回溯函数走向了不同的节点
for (int i = startIndex; i < nums.size(); i++) { // 既然是无序集合,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始
path.push_back(nums[i]); // 记录子集元素
backtracking(nums, i + 1); // 注意从i+1开始,元素不重复取,看图
path.pop_back(); // 回溯
}
完整代码
class Solution {
private:
vector path;
vector > result;
public:
vector> subsets(vector& nums) {
backtracking(nums, 0);
return result;
}
void backtracking(vector & nums, int startIndex) {
result.push_back(path); // 到每个节点都需要收集结果!
if (startIndex >= nums.size())
return;
for (int i = startIndex; i < nums.size(); ++i) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
return;
}
};
别忘了我们在推导回溯函数模板时,我们说过回溯函数的内容其实就是走到一个节点后,接下来应该做的事情。在这里我们到达一个节点,就应该收集结果,故一进backtracking函数就要往result添加结果
力扣题目链接/文章讲解/视频讲解
本题其实就是40.组合总和II和78.子集的缝合怪
40.组合总和II中最大的难题就是:站在一个节点的时候,有相同的前往后续节点的路径,我们要想办法跳过重复路径
我们当时的思路是用set记录在每个节点时,途径该节点已经去过的路径,这样就能够在前往下一个节点之前,看看这条路径是否已经去过,如果已经去过就跳过
当时需要注意的一个关键点就是需要先对数组排序!否则会出现重复的组合
78.子集的关键就是到每个节点都需要收集结果
这两种技巧缝合一下,这道题就出来了
class Solution {
private:
vector path;
vector > result;
public:
vector> subsetsWithDup(vector& nums) {
sort(nums.begin(), nums.end()); // 树层去重需要先排序
backtracking(nums, 0);
return result;
}
void backtracking(const vector & nums, int startIndex) {
result.push_back(path); // 到每个节点都需要收集结果
if (startIndex >= nums.size()) // 终止条件:当startIndex超过数组最大索引值,则返回
return;
unordered_set used; // 用于储存在该节点已经选择过的路径
for (int i = startIndex; i < nums.size(); ++i) { // 遍历可选择的路径,startIndex控制了在某个节点的可选择路径
if (used.find(nums[i]) != used.end()) // 如果已选择过这条路径,则跳过
continue;
used.insert(nums[i]); // 标记这条路径为“已选择”
path.push_back(nums[i]); // 添加记录
backtracking(nums, i + 1); // 前往这样一个节点:在该节点的可选择元素在数组中的索引为i+1到nums.size() - 1
path.pop_back(); // 撤销记录,回溯
}
return; // 返回
}
};
今天都是一些小技巧,个人感觉难点还是在于“如何知道在某个节点,有哪些路径可去”,往往是通过额外传入参数确定在该节点可选择路径的。有时,终止条件也可以通过这个额外参数确定,当额外参数所表达的是“在该节点无可选择路径了”,就说明到终止条件了
此外,别忘了我们最开始的推导:回溯函数处理树节点的过程就像是人在行走的过程。