对于集合、组合、排列等经典回溯问题,当所提供元素出现重复时,往往需要去重。
以集合问题为例,根据代码随想录的题解,一般有两种办法进行树层上的去重。
方法一:先排序,从startindex的第二个元素开始,如果和前一个元素相等,则跳过该元素。这里排序是很自然的想法,因为要将相同的元素放在一起,才能在遍历时轻松找到重复元素。代码如下(copy from 代码随想录):
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// 而我们要对同一树层使用过的元素进行跳过
if (i > startIndex && nums[i] == nums[i - 1] ) { // 注意这里使用i > startIndex
continue;
}
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0);
return result;
}
};
方法二:先排序,使用unordered_set记录当前层出现的元素,后续再出现时直接跳过,从而完成树层上的去重。代码如下(copy from 代码随想录):
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
unordered_set<int> uset;
for (int i = startIndex; i < nums.size(); i++) {
if (uset.find(nums[i]) != uset.end()) {
continue;
}
// 记录当前层出现的元素进行去重,注意不要传到下一层去
// 下一层会要一个新的uset记录出现过的元素
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0);
return result;
}
};
对于方法二,既然已经利用了额外空间记录出现过的元素,那么就可以很容易进行树层上的去重,为什么还要进行排序呢。这是因为去重时需要连续跳过全部重复元素。
举个例子,对[4,4,4,1,4]获取全部子集并去重。如果不排序,那么对于树的第二层(第一层是空集),选取第一个4完成回溯后,直接来到1的位置,而1的后面还有元素4,则会出现[1,4]这个集合,而在之前的回溯过程中,已经出现了[4,1]这个集合了,也就是出现了重复。实际上,不排序得到的结果还有[4,1,4]和[4,4,1]重复,以及[4,4,1,4]和[4,4,4,1]重复。可见原序列中的最后一个4对结果造成了影响,因此需要连续跳过全部重复元素。
当我们排序之后,就变成了对[4,4,4,4,1](或者[1,4,4,4,4])获取全部子集并去重。使用额外的空间记录该层出现过的元素,便能连续跳过全部重复元素,从而完成去重。
而对于另外一道题,491. 非递减子序列 - 力扣(LeetCode),也是要求对结果进行去重,然而不可能对输入进行排序,那要怎么办呢。其实因为输出的子集本身就要求是有序的,那么1后面的4必然会被跳过,那么输出自然就不会同时有[1,4]和[4,1]这种顺序相反的结果了。因此只需要使用额外的集合去记录某层出现过的元素,便能完成去重。