上来模仿着之前子集问题的去重逻辑,结果没能通过。原因是因为之前子集II问题去重是先对数组进行排序,然后进行树层去重。而本题要求收集递增子序列,就不能先排序,之前的去重逻辑就不适用了。那针对这个问题,我们应该如何选择去重逻辑呢?
1.一个最朴素也最容易想到的思路是set哈希表,针对同一树层的元素,之前用过的元素之后就不能再用。所以set哈希表每次回溯时需要进行重置,只能放在回溯函数内部,否则就不只是同一树层元素去重了。
class Solution {
private:
vector path;
vector> result;
void backtracking(vector& nums, int startIndex) {
if (path.size() >= 2 && isNondecreasing(path)) {
result.push_back(path);
}
unordered_set uset;
for (int i = startIndex; i < nums.size(); i++) {
if (uset.find(nums[i]) != uset.end()) //去重
continue;
uset.insert(nums[i]);
path.push_back(nums[i]); //加入路径数组
backtracking(nums, i + 1); //递归之后的元素
path.pop_back(); //回溯
}
}
bool isNondecreasing(vector nums) { //判断是否非递减
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i] > nums[i + 1])
return false;
}
return true;
}
public:
vector> findSubsequences(vector& nums) {
backtracking(nums, 0);
return result;
}
};
2.但是set去重还是太慢了,有没有更快一点的去重办法呢?注意到数组元素范围在[-100,100]内,元素个数不多,可以考虑用数组哈希表,相比于set减少了映射等时间。
class Solution {
private:
vector path;
vector> result;
void backtracking(vector& nums, int startIndex) {
if (path.size() >= 2 && isNondecreasing(path)) {
result.push_back(path);
}
int used[201] = {0}; //数组哈希表
for (int i = startIndex; i < nums.size(); i++) {
if (used[nums[i] + 100] == 1) //去重
continue;
used[nums[i] + 100] = 1; //用过的元素记录起来,下次不能用了
path.push_back(nums[i]); //加入路径数组
backtracking(nums, i + 1); //递归之后的元素
path.pop_back(); //回溯
}
}
bool isNondecreasing(vector nums) { //判断是否非递减
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i] > nums[i + 1])
return false;
}
return true;
}
public:
vector> findSubsequences(vector& nums) {
backtracking(nums, 0);
return result;
}
};
3.数组哈希表用时相对于set减少了一半,但是仍然排在后面。相信数组哈希表去重已经是很优秀的去重逻辑了,前面那些花费时间更少的题解是怎么做到的呢?回想起之前的经历,剪枝很多时候能帮助我们减少开销!之前的做法是我们找到一个符合大小的序列然后判断它是否非递减,如果非递减才加入返回数组。这样判断增加了许多时间开销,那能不能在收集路径顺序的时候就直接收集非递减序列呢?可以的,于是有了如下代码,时间开销大大减小,算法性能排在前列。
class Solution {
private:
vector path;
vector> result;
void backtracking(vector& nums, int startIndex) {
if (path.size() >= 2) {
result.push_back(path);
}
int used[201] = {0}; //数组哈希表
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back()) ||
used[nums[i] + 100] == 1) //去重
continue;
used[nums[i] + 100] = 1; //用过的元素记录起来,下次不能用了
path.push_back(nums[i]); //加入路径数组
backtracking(nums, i + 1); //递归之后的元素
path.pop_back(); //回溯
}
}
public:
vector> findSubsequences(vector& nums) {
backtracking(nums, 0);
return result;
}
};
1.这是首次遇到排列问题,那排列与组合有什么不同呢?举个例子,对于数组[1,2]的全组合只有一个[1,2]或[2,1],而它的全排列则有两个[1,2]和[2,1]。在组合问题中我们使用startIndex,每次递归时i+1,因为我们只需要找到一个[1,2]就行,2不用回头找1。但是在排列问题中,2需要回头找1,所以每次递归都需要从头开始,所以没必要再使用startIndex。数组不含重复元素,所以只需要考虑树枝去重。采用used数组,树枝递归过程用过的元素就标记一下。递归是从头开始的,遇见标记过的元素就跳过不再使用。结束当层递归就把元素弹出,并重新标记为未使用,那么树层循环就能找到这个元素了,相当于2能找到1了。
class Solution {
private:
vector path;
vector> result;
void backtracking(vector& nums, vector& used) {
//递归终止条件
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
//同一树枝递归过程中遇到用过的元素就跳过
if (used[i] == true)
continue;
//同一树枝上的元素,用过了标记一下,之后同一树枝的递归过程中不能再用
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
public:
vector> permute(vector& nums) {
// bool型used数组,大小和nums数组一样大,初始值全设为false
vector used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
1.本题是全排列的进阶,数组元素可以重复。按理说在上一题的基础上,给初始数组排个序,然后多个树层去重不就行了。根据之前的经验,我们写出树层去重代码if (i > 0 && nums[i] == nums[i - 1]) continue;。但我们发现测试用例[1,1,2]直接没过,问题出在哪呢?模拟代码的执行过程,我们发现第一层递归我们选择了1,第二层递归for循环的第一轮我们选择了第一个1,由于树枝去重的关系第一个1被跳过。这时候我们来到for循环的第二轮选择了第二个1,因为和前一个树是相等的这时候树层去重代码直接把第二个1也跳过了。这不对呀,这个1我们是要的啊。究其原因是因为第一个以1开头的排序数组还没有收集完成我们就把它跳过了,那什么时候它才算收集完成呢?根据代码可知一轮收集完成就是for循环完成一轮,在末尾处弹出并重新标记为false。所以我们的树层去重代码应该改为if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false continue;,表示在上一轮收集结束的情况下,如果开头元素还和之前相同,那就跳过。(其实used[i - 1] == true也能达到去重效果,本质上是所有树枝去重达到对前一位的去重,效率不如直接对树层去重。既然used[i - 1]设为true或false都能去重为什么不加不行呢?因为去重逻辑要保持used[i - 1]始终处于一种状态,不能一会true一会false)
class Solution {
private:
vector path;
vector> result;
void backtracking(vector& nums, vector& used) {
//递归终止条件
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
// used[i - 1] == true,说明同一树枝nums[i - 1]使用过
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
continue;
if (used[i] == false) {
//同一树枝上的元素,用过了标记一下
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
//同一树层上的元素,用过了解标记
used[i] = false;
}
}
}
public:
vector> permuteUnique(vector& nums) {
// bool型used数组,大小和nums数组一样大,初始值全设为false
vector used(nums.size(), false);
sort(nums.begin(), nums.end()); //树层去重要排序
backtracking(nums, used);
return result;
}
};
今日总结:烧脑。