文章目录
前置知识
参考前文
参考文章:
LeetCode刷题笔记【18】:回溯专题-1(回溯算法基础知识、用回溯算法解决组合问题)
LeetCode刷题笔记【19】:回溯专题-2(组合总和III、电话号码的字母组合)
LeetCode刷题笔记【20】:回溯专题-3(组合总和、组合总和II、分割回文串)
93.复原IP地址
题目描述
LeetCode链接:https://leetcode.cn/problems/restore-ip-addresses/description/
解题思路
思路: 两个子函数, 一个是backtrack
用于回溯遍历, 一个check
用于检查子串是否合法;
backtrack
中传入一个int
参数表示当前子串是ip
地址中的第几个, 如果是第4
个的话要直接查看从当前位到最后位是否合法;
check
中一方面检查数字是否在0-255
之间, 另一方面看是否有先导0
;
代码
class Solution {
private:
vector<string> ans;
// string path;
bool check(string& s, int left, int right){
if(left > right || right-left>3)
return false;
if(s[left]=='0' && left!=right)
return false;
int sum=0;
for(int i=1; right>=left; right--, i*=10){
sum += (s[right]-'0') * i;
}
if(sum > 255)
return false;
return true;
}
void backtrack(string& s, int left, int counter){
if(counter==4 && check(s, left, s.size()-1)){
ans.push_back(s);
return;
}
for(int right=left; right<s.size(); ++right){
if(check(s, left, right)){
s.insert(s.begin() + right + 1, '.');
backtrack(s, right+2, counter+1);
s.erase(s.begin() + right + 1);
}else{
break;
}
}
return;
}
public:
vector<string> restoreIpAddresses(string s) {
if (s.size() < 4 || s.size() > 12)
return ans;
backtrack(s, 0, 1);
return ans;
}
};
78.子集
题目描述
LeetCode链接:https://leetcode.cn/problems/subsets/description/
《代》经典模板 - 遍历树种所有节点
还是用经典回溯模板, 但是之前碰到的回溯是在"找叶子节点", 但是本题中其实是在"遍历整棵树, 记录所有节点"
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& 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();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
LeetCode题解思路 - 遍历沿途选择是否加入(寻找叶节点)
思路: 还是回溯, 但形式变为: 对于nums
中的每个数, 可以选择加入path
或者不加入path
, 然后遍历下一个数.
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, int index){
if(index==nums.size()){
ans.push_back(path);
return;
}
path.push_back(nums[index]);
backtrack(nums, index+1);
path.pop_back();
backtrack(nums, index+1);
return;
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtrack(nums, 0);
return ans;
}
};
观察这张图和上一张图, 思考二者异同
后者, 最后还是在求叶子节点, 不过过程中的分支是"是否加入当前节点"
前者, 在遍历并记录整棵树的所有节点, 分支变为"选择当前节点之后的哪个节点进行遍历"
90.子集II
题目描述
LeetCode链接:https://leetcode.cn/problems/subsets-ii/description/
解题思路
思路: 参考<78. 子集>
但是需要添加去重操作, 其实也不难, 也就是当nums[i]==nums[i+1]
的时候continue
罢了
《代》经典模板
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, int index){
ans.push_back(path);
if(index==nums.size()){
return;
}
for(int i=index; i<nums.size(); ++i){
if(i>index && nums[i]==nums[i-1])
continue;
path.push_back(nums[i]);
backtrack(nums, i+1);
path.pop_back();
}
return;
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backtrack(nums, 0);
return ans;
}
};
LeetCode思路
此时需要在"没有选择上一个元素, 并且当前元素和上一个元素相同"时, 跳过当前元素
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, int index, bool selected){// 所以这里加了一个bool类型变量记录上一个节点是否被选择
if(index==nums.size()){
ans.push_back(path);
return;
}
backtrack(nums, index+1, false);
if(!selected && index>0 && nums[index]==nums[index-1])
return;
path.push_back(nums[index]);
backtrack(nums, index+1, true);
path.pop_back();
return;
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backtrack(nums, 0, false);
return ans;
}
};
总结
今天的题可以看出: 将回溯过程抽象为树, 对于我们理解回溯是非常有帮助的, 很复杂的过程, 转化为"求叶子节点/遍历记录所有节点", 就变得十分清晰.
于此同时, 针对回溯问题, 我们已经学习了组合, 子集, 排列, 分割. 即将学完,
这几类问题解法相似, 但是各有各的特点和细节需要注意, 过程中要注意对比总结, 在最后学完了回溯的所有题型后, 做个总结.