文章目录
- 93.复原IP地址
- 思路
- 确定非法的范围
- 树形结构
- 伪代码
- 78.子集
- 思路
- 伪代码实现
- CPP代码
- 90.子集II
- 思路
- CPP代码
- 用used去重的办法
- 用set去重的版本
- 不使用used数组、set的版本
93.复原IP地址
力扣题目链接
文章讲解:93.复原IP地址
视频讲解:回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址
状态:一种十分典型的分割问题,根据IP地址的特征从分割一个字符、两个字符、三个字符推进,但是其中肯定是要进行剪枝的。代码的实现应该是什么样的呢?
思路
确定非法的范围
- 字符中出现非法字符串;
- 数字前面不允许有0:“1.013.1.1”;
- 不允许大于255。
这种分割问题,我们一定要用暴力枚举每一种分割的情况。
树形结构
树形结构中仍然还有许多细节需要注意:切割线如何固定在那里?然后剩余的切割线还能到后面继续切割?
如何去表达切割子串呢?
伪代码
-
递归的返回值和参数:
- 首先我们必须要有一个全局变量来记录结果
vector<string> result
- 其次,我们需要定义指示分割线的变量和我们添加逗点的数量
- pointNum:我们的结果一定是要有逗点的,所以树的深度只有三层(不算根)
vector<string> result; void backtracking(string& s, int startIndex, int pointNum){ }
- 首先我们必须要有一个全局变量来记录结果
-
递归终止条件:本题题目明确要求只分四段,也就如上文所说,3个pointNum就说明字符串分成四段了,我们检查一下第四段是否合法,就可以放入到结果集中了。
if (pointNum == 3){ //逗点数量为3时,分割结束
//判断第四段子字符串是否合法,如果合法就放入result中
if (isValid(s, startIndex, s.size() - 1)){
result.push_back(s);
}
return ;
}
-
单层搜索逻辑
-
本题中最重要的逻辑我觉得是没分割一次,检查一次子串合法性,如何加上逗点,不合法直接剪掉分支。
-
然后就是递归和回溯的过程:
- 递归调用时,下一层递归的
startIndex从i + 2
开始,因为我们在字符串里加了逗点。同时记录分割符的数量pointNum + 1
。 - 回溯时,记得删除逗点,然后
pointNum
也要-1
for (int i = startIndex; i < s.size(); i++){ if (isValid(s, startIndex, i)){ s.insert(s.begin() + i + 1, '.'); //在i的后面插入一个逗点 pointNum++; backtracking(s, i+ 2, pointNum); pointNum--; s.erase(s.begin() + i + 1); // 回溯删掉逗点 }else break; }
- 递归调用时,下一层递归的
-
-
判断子串是否合法
- 段位以0开头的数字不合法
- 段位里有非正整数字符不合法
- 段位如果大于255不合法
bool isValid(const string& s, int start, int end){ if (start > end){ return false; } if (s[start] == '0' && start != end) //0开头的数字不合法 return false; int num = 0; for (int i = start; i <= end; i++){ if (s[i] > '9' || s[i] < '0') return false;//遇到非法数字 num = num * 10 + (s[i] - '0'); if (num > 255) return false; //如果大于255 } return true; }
78.子集
力扣题目链接
文章讲解:78.子集问题
视频讲解:回溯算法解决子集问题,树上节点都是目标集和! | LeetCode:78.子集
状态:第一次碰到子集 问题毫无思路,但是看完卡哥的树形结构和讲解,真的是豁然开朗
思路
啥也不说,先上树形结构图
从图中可以很明显得看出:组合问题和切割问题都是收集树的叶子结点,而自己问题是找树的所有结点!
但是究其本质,子集问题仍然是一个组合问题,因为它的集合是无序的,子集{1, 2}{2, 1}是一样的。
所以既然是无序,取过的元素不会重复取,那么我们写回溯算法,for循环仍然要从startIndex开始而不是从0开始。(后序的排列问题我们for就得从0开始啦,因为{1, 2}和{2, 1}是两个集合)
伪代码实现
-
递归函数参数
- 全局变量path和result
- startIndex
vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& nums, int startIndex) { }
-
递归终止条件:从树形结构可以看出,剩余集合为空的时候,我们的递归就结束了。startIndex已经大于数组的长度了,就终止了,因为没有元素可取了
if (startIndex >= nums.size()) {
return;
}
- 单层递归逻辑:子集问题不需要任何剪枝,因为子集就是要遍历整颗树!
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]); // 子集收集元素
backtracking(nums, i + 1); // 注意从i+1开始,元素不重复取
path.pop_back(); // 回溯
}
CPP代码
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;
}
};
90.子集II
力扣题目链接
文章讲解:90.子集II
视频讲解:回溯算法解决子集问题,如何去重?| LeetCode:90.子集II
状态:这个问题就是一个:去重。在之前的40.组合总和II中我们已经讨论过回溯算法中的去重问题。
思路
去重思路和40.组合总和II一致,这里直接给出代码
CPP代码
用used去重的办法
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 而我们要对同一树层使用过的元素进行跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, i + 1, used);
used[i] = false;
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0, used);
return result;
}
};
用set去重的版本
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
unordered_set<int> uset;
for (int i = startIndex; i < nums.size(); i++) {
if (uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0);
return result;
}
};
不使用used数组、set的版本
本题也可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。
如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// 而我们要对同一树层使用过的元素进行跳过
if (i > startIndex && nums[i] == nums[i - 1] ) { // 注意这里使用i > startIndex
continue;
}
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0);
return result;
}
};