1.leetcode 93.复原IP地址
cpp
class Solution {
public:
vector<string> result;// 记录结果
// 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法
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;
}
// startIndex: 搜索的起始位置,pointNum:添加逗点的数量,相当于树形结构的深度
void backtracking(string& s,int startIndex,int pointNum){
if(pointNum==3){// 逗点数量为3时,分隔结束
// 判断第四段子字符串是否合法,如果合法就放进result中
if(isValid(s,startIndex,s.size()-1)){
result.push_back(s);
}
return;
}
for(int i=startIndex;i<s.size();i++){
if(isValid(s,startIndex,i)){// 判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin()+i+1,'.');// 在i的后面插入一个逗点
pointNum++;
backtracking(s,i+2,pointNum);// 插入逗点之后下一个子串的起始位置为i+2
pointNum--;// 回溯
s.erase(s.begin()+i+1);// 回溯删掉逗点
}
else break;// 不合法,直接结束本层循环
}
}
vector<string> restoreIpAddresses(string s) {
if(s.size()<4||s.size()>12) return result;
backtracking(s,0,0);
return result;
}
};
思路总结:
这道题目相信大家刚看的时候,应该会一脸茫然。
其实只要意识到这是切割问题,切割问题就可以使用回溯搜索法把所有可能性搜出来。
切割问题可以抽象为树型结构。
最后就是在写一个判断段位是否是有效段位了。
主要考虑到如下三点:
段位以0为开头的数字不合法
段位里有非正整数字符不合法
段位如果大于255了不合法
在分割回文串中我列举的分割字符串的难点,这道题基本都覆盖了,而且本题还需要操作字符串添加逗号作为分隔符,并验证区间的合法性,可以说是分割回文串的加强版了,如果大家能把树形结构图画出来的话,应该就能很好的理解题意。
2.leetcode 78.子集
cpp
class Solution {
public:
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();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return result;
}
};
思路总结:
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
有同学问了,什么时候for可以从0开始呢?
求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的博客就会讲到的。
在注释中,我们可以看到可以不写终止条件,因为我们本来就要遍历整棵树。有的人可能会担心不写终止条件会不会无限递归?并不会,因为每次递归的下一层就是从i+1开始的。
3.leetcode 90.子集II
cpp
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex,vector<bool>& used){
result.push_back(path);
if(startIndex>=nums.size()){
return;
}
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();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());//对于这种去重,我们先要对其进行排序
backtracking(nums,0,used);
return result;
}
};
补充:本题也可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。
cpp
class Solution {
public:
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();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());//去重需要排序
backtracking(nums,0);
return result;
}
};
思路总结:这道题目和上一题的区别在于集合里面就已经有重复的元素了,而且求取的子集要去重。这里我们要理解什么是树层去重和树枝去重,这个很重要。注意:去重需要先对集合排序。
同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!如果之前的子集问题和去重问题都可以掌握的好,这道题目可以分分钟AC。