今天的第1题对分割用比较复杂的形式进行回顾,第2、3题开始涉及子集问题。其中第1道题做的不容易,要注意很多细节。后面2道题要注意分清楚排列、组合、分割、子集的区别。要特别注意在递归函数循环遍历时不要把下一层递归的begin实参i + 1错写成begin + 1。
第1题(93. 复原IP地址)自己的实现思路是与day 27中的131. 分割回文串类似,在已知上一个分割点的情况下,在递归函数中循环遍历下一个可能的分割点。这样一来,两个分割点,或者说起始和末尾下标,就决定了当前段的内容。将符合要求的当前段内容加入path,再将当前段末尾的下标 + 1传入递归函数作为新的起始下标,就实现了回溯。不过这道题需要注意的细节很多:
- 如果当前begin对应数字是0,要做特殊处理,将其放入path后进行回溯后直接return,因为以0开始的数字段只能是0本身,其他的不符合题目要求;
- 在对path回溯时,因为path中还添加了一个或多个小数点,所以不能用.substr(0, begin)取,否则会错误地取太少;
- 对当前段数值是否大于255判断时,应当提前一轮循环就进行判断,否则有可能出现上一轮值符合要求,但这一轮就大于255的情况;而提前一轮计算要注意避免越界;
- 递归出口则是段数达到4,或者起始下标已经超过数组s范围。但只要前面两个条件同时满足的情况下,才是一种符合要求的分割方案,才能将path放入到res中;
- 上面一条属于纵向的剪枝,而横向也是可以剪枝的。当s的剩余部分与当前path中段数量无法凑够4段,或凑成的段数至少大于4段时,就没有必要再进行;此外,由于符合要求的最大值是255,是3位数,所以在数字超过3位时必定是大于255的,也可剪枝,所以循环最多进行3次,具体进行的次数应该从3,和当前begin与末尾的距离中取较小值,避免越界。
class Solution {
public:
string path;
vector<string> res;
void back(string& s, int begin, int cnt) {
if (begin == s.size() || cnt == 4) {
if (begin == s.size() && cnt == 4) {
res.push_back(path.substr(0, path.size() - 1));
}
return;
}
if (s[begin] == '0') {
path += "0.";
back(s, begin + 1, cnt + 1);
path = path.substr(0, path.size() - 2);
return;
}
int num = s[begin] - '0';
int endMax = min(begin + 3, (int)s.size());
int lenRemain = s.size() - begin;
for (int i = begin;
i < endMax &&
num <= 255 &&
cnt + lenRemain >= 4 &&
cnt + ceil(lenRemain / 3) <= 4;
++i) {
int len = i - begin + 1;
path += s.substr(begin, len);
path += ".";
back(s, i + 1, cnt + 1);
path = path.substr(0, path.size() - (len + 1));
if (i < endMax - 1) {
num = 10 * num + s[i + 1] - '0';
lenRemain = s.size() - (i + 1);
}
}
return;
}
vector<string> restoreIpAddresses(string s) {
path.clear();
res.clear();
back(s, 0, 0);
return res;
}
};
代码在计算上面最后一条中,凑成的段数是否至少大于4段的过程中,需要用到对计算结果向上取整的ceil()函数。ceil()与向下取整的floor()相对应。
题解的解法整体上与自己的思路一致,实现上有几处不同:
- 没有在回溯函数中判断当前段数字是否合法,而是抽象为了函数。这样做相对自己的方法少了横向剪枝,但对其他非数字的异常符号有了判断,并且将0开头的情况整合在其中。这一题中题目描述只会有数字,所以不对非数字内容进行判断也能AC;
- 在对path添加或删除元素时,没有用“+”或string.substr(),而是用string.insert()和string.erase(),且是对s直接进行操作,没有设立path;
- 在主函数中,用
if (s.size() < 4 || s.size() > 12) return res;
进行了剪枝。
结合了题解代码的优点后,重新写出的代码如下:
class Solution {
public:
vector<string> res;
void back(string& s, int begin, int cnt) {
if (begin == s.size() || cnt == 4) {
if (begin == s.size() && cnt == 4) {
res.push_back(s.substr(0, s.size() - 1));
}
return;
}
if (s[begin] == '0') {
s.insert(s.begin() + begin + 1, '.');
back(s, begin + 2, cnt + 1);
s.erase(s.begin() + begin + 1);
return;
}
int num = s[begin] - '0';
int endMax = min(begin + 3, (int)s.size());
int lenRemain = s.size() - begin;
for (int i = begin;
i < endMax &&
num <= 255 &&
cnt + lenRemain >= 4 &&
cnt + ceil(lenRemain / 3) <= 4;
++i) {
s.insert(s.begin() + i + 1, '.');
back(s, i + 2, cnt + 1);
s.erase(s.begin() + i + 1);
if (i < endMax - 1) {
num = 10 * num + s[i + 1] - '0';
lenRemain = s.size() - (i + 1);
}
}
return;
}
vector<string> restoreIpAddresses(string s) {
res.clear();
if (s.size() < 4 || s.size() > 12) {
return res;
}
back(s, 0, 0);
return res;
}
};
实现过程中还是遇到了些细节问题:
- 第7行,在向res中添加s时,不能在这里对s进行erase(),否则向上回溯时后错误地多删;
- string.insert()和string.erase()的用法还不熟悉;
- 回溯时传入的新begin应该时i + 2,而不是i + 1,因为还插入了点符号;
- 对于0的情况,传入的参数应该是begin + 2,而不是2。
二刷:stoi()对应头文件是<string>,isdigit()和isalaph()对应头文件是<cctype>,floor()和ceil()对应头文件是<cmath>。二刷自己实现的代码方法更简单和容易理解:
#include<string>
#include<cctype>
class Solution {
public:
vector<string> res;
bool isOK(string s) {
for (char c : s) {
if (!isdigit(c)) {
return false;
}
}
if (s.size() > 1 && s[0] == '0') {
return false;
}
int num = stoi(s);
if (num < 0 || num > 255) {
return false;
}
return true;
}
void back(string s, string path, int begin, int level) {
if (level == 4) {
if (begin == s.size()) {
res.push_back(path.substr(0, path.size() - 1));
}
return;
}
for (int i = begin; i < s.size(); i++) {
if (i >= begin + 3) {
break;
}
string sub = s.substr(begin, i - begin + 1);
if (!isOK(sub)) {
break;
}
back(s, path + sub + ".", i + 1, level + 1);
}
return;
}
vector<string> restoreIpAddresses(string s) {
res.clear();
back(s, "", 0, 0);
return res;
}
};
第2题(78. 子集)尝试按照模板写却没有成功,得到的结果总是会少一些本应空缺一些数字的答案,或又会有重复。于是改用77. 组合中自己的方法,才AC了。分析原因,是模板写法总是在取元素,只在取某个元素后对其继续进行递归。而自己的写法是不论取或不取,都进行递归,相比前者会对不取元素的情况进行处理,所以像这种取子集的题就很适合。
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void back(vector<int>& nums, int begin) {
if (begin == nums.size()) {
res.push_back(path);
return;
}
back(nums, begin + 1);
path.push_back(nums[begin]);
back(nums, begin + 1);
path.pop_back();
return;
}
vector<vector<int>> subsets(vector<int>& nums) {
path.clear();
res.clear();
back(nums, 0);
return res;
}
};
题解还是使用原来的模板。思路是排列、组合、分割都是针对回溯树的叶子节点,取满足要求的叶子节点。其中排列与组合的不同是:
- 顺序对于组合不重要,所以元素和个数相同的话就被认定为同一个组合,所以实现时,在循环遍历中的起始下标应该从begin开始,而非0;
- 顺序对于排列重要,所以即便元素和个数相同,只要顺序不一致就不被认定为同一个排列,所以实现时,在循环遍历中的起始下标应该从0开始,而非begin;
而这一题要取的是所有节点,不只是叶子节点。所有节点恰好满足不重复且是子集的要求,所以在实现时也只需要套用模板,但需要把res.push_back(path)从递归出口移到函数开始处,让每次递归,即每个节点都能运行这一句,使每个节点都被放入到结果当中。
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void back(vector<int>& nums, int begin) {
res.push_back(path);
if (begin == nums.size()) {
return;
}
for (int i = begin; i < nums.size(); ++i) {
path.push_back(nums[i]);
back(nums, i + 1);
path.pop_back();
}
return;
}
vector<vector<int>> subsets(vector<int>& nums) {
path.clear();
res.clear();
back(nums, 0);
return res;
}
};
其中第7~9行的递归出口是不必要的,因为即便不加,当begin到达数组末尾时也会因无法开始循环而return。另外,自己在实现时,又又一次因为把back()中的i + 1错写成了begin + 1。
二刷:把res.push_back(path)放到了回溯函数的循环中,也AC了但需要手动添加空集,题解的放在回溯函数一开始处的方式更简洁。
第3题(90. 子(集II)是day 27中40. 组合总和II和上一题(78. 子集)的组合。首先在待选数组中有重复元素,在答案的一个集合中也可以包含数量不超限的重复元素,但不同集合不能相等。与40. 组合总和II一样要求在回溯树的同一条树枝上可以重复,但在同一层内不能选已经被选过的重复元素。所以要像40. 组合总和II一样对待选数组排序。然后在递归函数循环遍历时,将每段重复区间的第一个放进path,以第一个元素的下一个位置作为下一个begin进行递归。
这里选用40. 组合总和II里面最简便的第3种实现方式,即添加每个重复区间的第一个元素并递归后,对当前重复区间的其他元素continue。类似这种情况有2种解决方式,第1种是用num[i]和nums[i - 1]判断,第2种是用num[i]和nums[i + 1]判断。前者适用于要取重复区间第一个元素的情况,也就是这一题的情况;后者则适用于要取重复区间最后一个元素的情况。所以进行continue的条件要写成if (i > begin && nums[i] == nums[i - 1])。
然后,这一题也是子集问题,所以要像上一题(78. 子集)一样把“path添加进res的时机”从递归出口移动到函数开始,记录所有叶子和非叶子节点。
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void back(vector<int>& nums, int begin) {
res.push_back(path);
if (begin == nums.size()) {
return;
}
for (int i = begin; i < nums.size(); ++i) {
if (i > begin && nums[i] == nums[i - 1]) {
continue;
}
path.push_back(nums[i]);
back(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
path.clear();
res.clear();
sort(nums.begin(), nums.end());
back(nums, 0);
return res;
}
};
第7~9行的递归出口同样可以去掉。因为去掉的话,当begin超范围,也还是会因为进行不了之后的for循环而return。
如果使用usedInBranch表的方式,则代码为:
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
vector<bool> usedInBranch;
void back(vector<int>& nums, int begin) {
res.push_back(path);
if (begin == nums.size()) {
return;
}
for (int i = begin; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i - 1] && usedInBranch[i - 1] == false) {
continue;
}
path.push_back(nums[i]);
usedInBranch[i] = true;
back(nums, i + 1);
usedInBranch[i] = false;
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
usedInBranch.resize(nums.size(), false);
path.clear();
res.clear();
sort(nums.begin(), nums.end());
back(nums, 0);
return res;
}
};