资料来源:代码随想录
9.分割回文串 131
本题有如下几个难点:
- 切割问题其实类似组合问题(也可以画树形结构)
- 如何模拟那些切割线(startIndex)
- 切割问题中递归如何终止(startIndex到size了)
- 在递归循环中如何截取子串
- 如何判断回文(双指针法)
切割问题和组合问题的思想是类似的,组合问题中选取哪个元素,切割问题中就是从哪个元素开始切割。例如aab,组合问题中选第一个a,切割问题中就是从第一个a后面切开,选第二个a就是从第二个a后面切开。
因为是在一个集合中切割,相当于从一个集合中选取元素进行组合,所以也要用到startIndex来避免切到重复的元素。在组合问题中,startIndex代表从这个元素开始往后进行选取,在切割问题中就是从这个元素开始切割。
切割出来的子串应该如何表示呢?[startIndex,i]。这是因为每一层的startIndex是一样的,是上一层确定了的切割线,但从左往右i是一直在递增的,是这一层的切割线,所以从startIndex到i就是切出来的子串。
class Solution {
private:
vector<vector<string>> result; //结果集
vector<string> path; //最终切割好后的结果也在叶子节点收集,所以一个结果还是相当于一条路径
void backtracking(const string& s, int startIndex)
{
//终止条件:当startIndex>s的大小时,说明这一条路径已经切到最后了
if(startIndex>=s.size())
{
result.push_back(path); //这里没有判断是否是回文子串就直接把结果存起来,是因为是否是回文子串会在单层递归逻辑中判断
return;
}
//单层递归
for(int i=startIndex; i<s.size(); i++)
{
//判断切割出来的[startIndex,i]是否是回文子串
if(isPalindrome(s,startIndex,i)) //是回文子串
{
string str=s.substr(startIndex,i-startIndex+1); //把[startIndex,i]从s中取出来
path.push_back(str);
}
else //不是,则直接跳过
{
continue;
}
backtracking(s,i+1); //下一层不能和这一层重复切割
path.pop_back(); //回溯
}
}
//判断是否是回文子串:双指针法,一个从前向后,一个从后向前,如果两个指针指向的元素始终相同,说明是回文子串
//这个函数一定是写在递归函数外面的
bool isPalindrome(const string& s, int start, int end)
{
for(int i=start,j=end; i<=j; i++,j--)
{
if(s[i]!=s[j])
{
return false; //前后不相等,则不是
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s,0);
return result;
}
};
10.复原IP地址 93
往字符串中间插入点,也是相当于在分割字符串,是在处理输入的源字符串,最后把所有符合要求的加入字符串类型数组。
本题不是组成新的结果,而是修改传入的字符串!
分割过程:
解释num=num*10+(s[i]-'0'):
class Solution {
private:
vector<string> result; //把所有可能结果放进一个字符串类型数组
void backtracking(string& s, int startIndex, int pointNum) //需要避免上下层之间重复切割;需要记录插入.的数量
//注意,这里不能是const string&s这样引用,下面s.insert等迭代器需要更改传入的字符串,所以应该用复制的方式传入
{
//终止条件:最多分成四段,所以最多插入三个.就应该结束了
if(pointNum==3)
{
if(isValid(s,startIndex,s.size()-1)) //判断最后一段是否合法,是的话把处理好的字符串s存进结果
{
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,'.'); //在截取的子串后面插入.
pointNum++;
backtracking(s,i+2,pointNum); //因为还插入了一个点,所以下次分割(下一层递归)从i+2的地方开始,即下一层递归的startIndex是本层的i+2
pointNum--; //回溯
s.erase(s.begin()+i+1); //回溯,把插入的.删掉
}
else //不满足要求,后面的都可以不用看了,所以直接结束本层循环
{
break;
}
}
}
//判断子串是否符合地址的条件
bool isValid(const string& s, int start, int end) //这个不需要改字符串,所以可以用const传入
{
if(start>end) return false;
if(s[start]=='0' && start!=end) return false; //以0开头,非法
int num=0;
for(int i=start; i<=end; i++)
{
if(s[i]>'9' || s[i]<'0') return false; //不在0-9范围内,非法
num=num*10+(s[i]-'0'); //s[i]-'0'是把字符转化为数字
if(num>255) return false; //子串超过255,说明数字长度超了,非法
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
result.clear();
if(s.size()<4 || s.size()>12) return result; //初步剪枝
backtracking(s,0,0);
return result;
}
};
11.子集问题 78
本题和组合、切割问题不同的地方在于:组合和切割问题都是到了叶子节点才会收集一条路径,本题需要访问树形结构里的所有节点,所以每到一个节点,都要收集一次路径。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
{
result.push_back(path);
//把收集结果的语句放在这里,是因为每一次递归包含最后一个节点的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;
}
};
13.子集II 90
子集问题可以不用写递归函数里的终止条件。因为单层递归的循环条件是i<size,而i是从startIndex开始的,但每一层递归传进去的startIndex都是i+1,所以随着递归层数的增加,startIndex是一直在增加的,迟早会不满足i<size的条件,递归就会结束,最终还是会返回的。
本题和上一题的区别是:本题中的集合有重复元素,但最终的结果集中不允许有重复的结果,所以需要在上一题的基础上加上去重的过程。
class Solution {
private:
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++)
{
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();
sort(nums.begin(),nums.end()); //去重之前要排序
vector<bool> used(nums.size(),false);
backtracking(nums,0,used);
return result;
}
};
14.递增子序列 491
题目中要求不能有重复的子集,所以需要去重。但从以往的去重过程来看,去重之前需要先对原集合进行排序。本题不可以进行排序,不然的话,集合里全都是递增子序列了。要在原集合的顺序上,直接挑选能构成递增子序列的元素。
用set来去重。定义一个set来存放本层已经访问过的元素,每遇到一个元素,就和set里的元素对比一下看是否本层已经有过了。每层递归都会重新定义一个set,所以不需要回溯。
continue和break的区别:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
{
//收集结果:要求子序列中至少有两个元素
if(path.size()>1)
{
result.push_back(path);
//return; 不可以在这收集结果之后就return,因为要取树上所有的点
}
unordered_set<int> uset; //每层递归都会重新定义一个set来存放本层已经访问过的元素
for(int i=startIndex; i<nums.size(); i++)
{
//新加入的元素如果小于path中最后一个元素,说明不是递增序列,跳过这个,注意要先判断path是否为空!先判断是否为空!
//如果set中已经有和新加入元素相等的元素,说明重复了,跳过这个
if((!path.empty() && nums[i]<path.back()) || (uset.find(nums[i])!=uset.end()))
{
continue;
}
uset.insert(nums[i]); //把当前元素放入set
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums,0);
return result;
}
};
15.全排列 46
和组合、分割、子集问题不同的是,排列问题中序列是有序的,[1,2]和[2,1]是两个不同的序列,而在其它问题中二者是同一个序列。在排列问题中,元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但因为同一个排列中不能把相同的元素用两遍,即一个树枝上不能吧一个元素取两遍,所以还需要一个used数组来标记在同一树枝上已经使用过哪些元素。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool> used)
{
//终止条件:到叶子节点即构成全排列,可收集结果
if(path.size()==nums.size())
{
result.push_back(path);
return; //不需要收集树中的所有节点,所以收集完一个结果后可以return结束递归
}
for(int i=0; i<nums.size(); i++)
{
if(used[i]==true) continue; //全排列中不能出现重复的元素,所以如果某元素已经在path中收集过了,则跳过
used[i]=true;
path.push_back(nums[i]);
backtracking(nums,used);
used[i]=false; //回溯
path.pop_back();
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return result;
}
};
16.全排列II 47
跟上一题的区别是:上一题给出的数组nums中是没有重复元素的,而本题中有重复元素,并且需要返回的是不重复的排列结果。那么就涉及到了去重的问题。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool> used)
{
if(path.size()==nums.size())
{
result.push_back(path);
return;
}
for(int i=0; i<nums.size(); i++)
{
if((used[i]==true) || (i>0 && nums[i]==nums[i-1] && used[i-1]==false)) //用过的元素和重复的元素都要跳过
{
continue;
}
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,used);
used[i]=false;
path.pop_back();
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
backtracking(nums,used);
return result;
}
};
一个小小的总结:
到现在为止,已经学习了回溯算法的组合、分割、子集、排列问题。组合、分割、子集问题中,组合都是无序的,[1,2]和[2,1]是相同的结果,所以前面取过1之后,后面就不能再取1了,那么就需要startIndex(不全是这样,如果是从不同的集合中取元素、相互之间不影响的话,也不需要)。但排列问题中[1,2]和[2,1]是不同的结果,所以前面取过1之后,后面可以再取,就不需要startIndex了。但是因为要避免同一树枝上把相同的元素取两遍,所以需要used数组来标明哪些元素已经用过了。
去重有两种方法:nums[i]==nums[i-1]&&used[i-1]==false是一种方法,我觉得这种方法比较好用,这种方法需要在去重之前进行排序,所以不适用于本身有大小要求的题目。比如递增子序列,如果在去重之前对原数组进行排序,那么所有结果都会是递增子序列,就不符合要求了,所以不能用这种去重方法,改用set进行去重。用set去重:定义一个unordered_set,存放本层已经访问过的元素,要取某个元素之前现在set里找一下,如果能找到相同的元素,说明会重复,就要跳过当前元素。
后面还有三道hard题目没有做,有空再来看看吧!