例题1:分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串。返回 s
所有可能的分割方案。
这道题涉及到字符串的切割,其实跟组合问题有点像:
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。
在树层的遍历中,我们每次都选下1个元素作为切割对象;
在树枝的遍历中,我们根据树层选择的元素为第1个切割点,切割剩下的数据。
- 递归函数参数
全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里)
本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
void backtracing(string& s, int startindex)
- 终止条件
当 startindex 到了字符串 s 的最后,终止遍历,将当前保存的 tmp 字符串数组加入 res。(我们在单层循环逻辑中判断当前子串是否是回文,保证正确性)
- 单层搜索的逻辑
在每一层循环中,我们先寻找一个开始切割的点,根据这个切割点进行递归。具体来说,寻找了一个切割点后,我们判断在这个切割点的右边先寻找一个回文串,把该回文串存入我们的一维数组,然后将该回文串的右边作为新的切割点,递归切割剩下的子串。
class Solution {
public:
vector<vector<string>> res;
vector<string> tmp;
bool ishuiwen(string &substring){//判断回文串
string s = substring;
std::reverse(substring.begin(),substring.end());
if(s == substring)
return true;
return false;
}
void backtracing(string& s, int startindex){
if(startindex == s.size()){
res.push_back(tmp);
return;
}
for(int i = startindex; i < s.size(); i++){
string str = s.substr(startindex, i - startindex + 1);
if(ishuiwen(str)){//左闭右闭区间
tmp.push_back(str);//选取符合回文的子串
}
else
continue;
//选好了,继续切割下一个
backtracing(s, i+1);
tmp.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtracing(s,0);
return res;
}
};
例题2:复原IP地址
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
这道题可以说是例题1的加强版,因为还涉及到逗点的增加。
所以在调用函数的参数中,还会使用一个 numpoint 来表示已经被我们使用了多少个逗点了。
终止条件就是字符串已经被我们用 3 个逗点分成了 4 段。
单层循环逻辑:
跟例题1类似,先判断当前子串是否合法,合法的话我们加入1个逗点,继续判断下一个子串。我一开始想用字符串类型来存储、添加、删除子串,但是删除的操作成本比较高,于是我们直接在原字符串上添加、删除逗点。
class Solution {
public:
vector<string> res;
bool iseffective(const string& s){
if(s.size()==0) return false;
if(s[0] == '0' && s.size() > 1)
return false;
int num = 0;
for (int i = 0; i < s.size(); i++) {
if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
return false;
}
num = num * 10 + (s[i] - '0');
if (num > 255) { // 如果大于255了不合法
return false;
}
}
return true;
}
void backtracing(string& s, int startindex, int num){
if(num == 0)//逗点加完了
{//这里还要检查第4段字符串是否合法
string str = s.substr(startindex, s.size()-1);
if(iseffective(str))
res.push_back(s);
return ;
}
for(int i = startindex; i < s.size(); i++){
string str = s.substr(startindex, i - startindex + 1);
if(iseffective(str)){
s.insert(s.begin()+i+1,'.');//insert为sting可用,const string不行
}
else
continue;
backtracing(s, i+2, num-1);//跳过新加的逗点
s.erase(s.begin()+i+1);
}
}
vector<string> restoreIpAddresses(string s) {
backtracing(s,0,3);
return res;
}
};
例题3:子集II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
- 这道题跟回溯中的组合法的题很像,都是“树层不可有重复数据,树枝可以有重复数据”,所以依然用一个 used 数组来实现树层的去重。所以调用函数的参数就不用说了,
void backtracing(vector<int>& nums, int startindex, vector<bool>& used)
- 但是终止条件有点不一样,这道题中,每个树枝的值都会作为结果的子集输出,所以没有终止条件,每次调用时首先把当层数据存入结果里
// if(startindex == nums.size()-1)
// {
// res.push_back(tmp);
// return ;
// }终止条件这样写不对
res.push_back(tmp);//树枝的每一层都可以放入数组中存储
单层循环逻辑:
- 我们每访问树层的数据时,把该数据的 used 位标为 true,然后开始递归(形成树枝),树枝上再遇到相同的数字,是可以放入当前结果集的。
- 如果我们访问树层数据时,发现两个数据一样。并且前一个数字的 used 位为 false,说明该数字的树枝已经形成了,我们再继续对这个数进行递归,就会产生重复的子集了。
class Solution {
public:
vector<vector<int>> res;
vector<int> tmp;
void backtracing(vector<int>& nums, int startindex, vector<bool>& used){
// if(startindex == nums.size()-1)
// {
// res.push_back(tmp);
// return ;
// }终止条件这样写不对
res.push_back(tmp);//树枝的每一层都可以放入数组中存储
for(int i = startindex; i < nums.size(); i++){
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false){//两个元素一样但被使用了
continue;
}
used[i] = true;
tmp.push_back(nums[i]);
backtracing(nums, i+1, used);
tmp.pop_back();
used[i] = false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
//必须要排序
sort(nums.begin(),nums.end());
backtracing(nums,0,used);
return res;
}
};
例题4:全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
全排列就是找出给定数组的所有元素不同的组合,跟组合不同的地方在于它每次都会查看并运用,所以我们要用 used 数组记录剩下还没用的数据有哪些。
- 递归函数参数
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
vector<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used)
- 递归终止条件
当收集元素数组 path 的大小达到与 nums 数组一样大时,说明找完了一个全排列。
- 单层搜索的逻辑
因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。
而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
整体代码如下:
class Solution {
public:
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) continue; // path里已经收录的元素,直接跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};