注意:在不清楚回溯模板是什么的可以先看前天的内容:
传送门:代码随想录算法训练营第二十二天(回溯 一)-CSDN博客
力扣题部分:
93.复原IP地址
题目链接:. - 力扣(LeetCode)
题面:
有效 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
中的任何数字。你可以按 任何 顺序返回答案。
思路:
这道题挺难的,但是如果前几天的回溯理解到位,还是可以独立完成的。
首先我们需要像昨天分割回文串一样分割题面给的字符串,可能有人觉得这里的分割不像前面一样简单——只能分三刀,并且分下来的长度又有要求,内容也有要求,而且分完还要要求不能有剩余等等......
我在解决的时候为了思路清晰,多设置了几个函数:
首先是change函数,功能是把所给的合法字符串变成数字,如果长度过长,有其他字符或者前导0,直接返回- 1 标志着这段字符串不符合要求。
然后是judgepart函数,功能是通过调用change函数,输入切割的开始和末尾,判断切割后的字符串是否合法。
递归的主题代码函数的切割手法和分割回文串是一样的,这里切割就不讲了,想了解的可以看昨天的131.分割回文串那题。
传送门:代码随想录算法训练营第二十三天(回溯 二)-CSDN博客
关于分割还有人可能会疑惑:不是最多只能切三刀吗?昨天的切割方法能直接套用吗?
其实加个计数器就好了,终止函数部分只有切第三刀后第四刀前时才能记录当前切法。
当然,为了保证每段符合要求,套回溯模板的for循环下需要判断截的部分合不合法(通过调用judgepart函数)
还有一个做题遇到的小问题没提过:答案的每种切法中的"."我是怎么加进去的呢?
我是通过创造一个字符串数组来记录切下来合法的每段字符串,在终止前把里面的字符串和"."拼起来再放入答案数组里的。
代码实现:
class Solution {
public:
vector<string>result;
string rightparts[6] = {""};
string rightip;
int change(string nums)
{
if(nums.size() > 3) return -1;
if(nums == "0") return 0;
bool flag0 = true;
int i, num = 0;
for(i = 0; i < nums.size(); i ++)
{
if(!isdigit(nums[i]) || nums[i] == '0' && flag0) return -1;
else flag0 = false;
num = num * 10 + nums[i] - '0';
}
return num;
}
bool judgepart(const string &s, int begin, int end)
{
string part = s.substr(begin,end);
if(change(part) >= 0 && change(part) <= 255) return true;
else return false;
}
void judgeip(const string &s, int cutindex, int count)
{
if(cutindex >= s.size())
{
if(count == 4)
{
rightip = "";
for(int i = 1; i < 4; i ++)
{
rightip += rightparts[i];
rightip += ".";
}
rightip += rightparts[4];
result.push_back(rightip);
}
return;
}
for(int i = cutindex; i < s.size(); i ++)
{
if(judgepart(s,cutindex,i - cutindex + 1) && count <= 4)
{
string rightpart = s.substr(cutindex, i - cutindex + 1);
rightparts[++count] = rightpart;
judgeip(s, i + 1, count);
count --;
}
}
}
vector<string> restoreIpAddresses(string s) {
judgeip(s, 0, 0);
return result;
}
};
78.子集
题目链接:. - 力扣(LeetCode)
题面:
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
思路:
这个就涉及到回溯法的不同与前面的功能了:分子集。
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
有同学问了,什么时候for可以从0开始呢?
求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。
以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
按照这样的思路和套用回溯模板,我们不难写出下面的代码:
代码实现:
class Solution {
public:
vector<vector<int>>result;
vector<int>son;
void findsets(vector<int> nums, int cutindex)
{
result.push_back(son);
if(cutindex >= nums.size()) return;
for(int i = cutindex; i < nums.size(); i ++)
{
son.push_back(nums[i]);
findsets(nums, i + 1);
son.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
findsets(nums, 0);
return result;
}
};
90.子集II
题目链接:. - 力扣(LeetCode)
题面:
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
思路:
对于看懂了昨天40.组合总和II和今天的上一个题目的人来说,这道题没有任何新的困难可言。
如果上面那道题看明白了,理解完去重的方法实现原理题目就会做了。
因为这个去重原理非常抽象,我就不再重复了,想看关键去重代码原理的可以看昨天题目40.组合总和II的内容:
传送门:代码随想录算法训练营第二十三天(回溯 二)-CSDN博客
代码实现:
class Solution {
public:
vector<vector<int>>result;
vector<int>son;
void findset(vector<int> nums, int cutindex, vector<bool> &used)
{
result.push_back(son);
if(cutindex >= nums.size()) return;
for(int i = cutindex; i < nums.size(); i ++)
{
if(i > 0 && nums[i - 1] == nums[i] && used[i - 1] == false) continue;
son.push_back(nums[i]);
used[i] = true;
findset(nums,i + 1, used);
son.pop_back();
used[i] = false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool>used(nums.size(),false);
sort(nums.begin(), nums.end());
findset(nums, 0, used);
return result;
}
};