|
| 1 | + |
| 2 | + |
| 3 | +> 在 [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) 中一位录友对 整颗树的本层和同一节点的本层有疑问,也让我重新思考了一下,发现这里确实有问题,所以专门写一篇来纠正,感谢录友们的积极交流哈! |
| 4 | +
|
| 5 | +接下来我再把这块再讲一下。 |
| 6 | + |
| 7 | +在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中的去重和 [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中的去重 都是 同一父节点下本层的去重。 |
| 8 | + |
| 9 | +[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢? |
| 10 | + |
| 11 | +我用没有排序的集合{2,1,2,2}来举例子画一个图,如图: |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +图中,大家就很明显的看到,子集重复了。 |
| 16 | + |
| 17 | +那么下面我针对[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) 给出使用set来对本层去重的代码实现。 |
| 18 | + |
| 19 | +# 90.子集II |
| 20 | + |
| 21 | +used数组去重版本: [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) |
| 22 | + |
| 23 | +使用set去重的版本如下: |
| 24 | + |
| 25 | +```C++ |
| 26 | +class Solution { |
| 27 | +private: |
| 28 | + vector<vector<int>> result; |
| 29 | + vector<int> path; |
| 30 | + void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) { |
| 31 | + result.push_back(path); |
| 32 | + unordered_set<int> uset; // 定义set对同一节点下的本层去重 |
| 33 | + for (int i = startIndex; i < nums.size(); i++) { |
| 34 | + if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass |
| 35 | + continue; |
| 36 | + } |
| 37 | + uset.insert(nums[i]); // set跟新元素 |
| 38 | + path.push_back(nums[i]); |
| 39 | + backtracking(nums, i + 1, used); |
| 40 | + path.pop_back(); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | +public: |
| 45 | + vector<vector<int>> subsetsWithDup(vector<int>& nums) { |
| 46 | + result.clear(); |
| 47 | + path.clear(); |
| 48 | + vector<bool> used(nums.size(), false); |
| 49 | + sort(nums.begin(), nums.end()); // 去重需要排序 |
| 50 | + backtracking(nums, 0, used); |
| 51 | + return result; |
| 52 | + } |
| 53 | +}; |
| 54 | + |
| 55 | +``` |
| 56 | +
|
| 57 | +针对留言区录友们的疑问,我再补充一些常见的错误写法, |
| 58 | +
|
| 59 | +
|
| 60 | +## 错误写法一 |
| 61 | +
|
| 62 | +把uset定义放到类成员位置,然后模拟回溯的样子 insert一次,erase一次。 |
| 63 | +
|
| 64 | +例如: |
| 65 | +
|
| 66 | +```C++ |
| 67 | +class Solution { |
| 68 | +private: |
| 69 | + vector<vector<int>> result; |
| 70 | + vector<int> path; |
| 71 | + unordered_set<int> uset; // 把uset定义放到类成员位置 |
| 72 | + void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) { |
| 73 | + result.push_back(path); |
| 74 | +
|
| 75 | + for (int i = startIndex; i < nums.size(); i++) { |
| 76 | + if (uset.find(nums[i]) != uset.end()) { |
| 77 | + continue; |
| 78 | + } |
| 79 | + uset.insert(nums[i]); // 递归之前insert |
| 80 | + path.push_back(nums[i]); |
| 81 | + backtracking(nums, i + 1, used); |
| 82 | + path.pop_back(); |
| 83 | + uset.erase(nums[i]); // 回溯再erase |
| 84 | + } |
| 85 | + } |
| 86 | +
|
| 87 | +``` |
| 88 | + |
| 89 | +在树形结构中,**如果把unordered_set<int> uset放在类成员的位置(相当于全局变量),就把树枝的情况都记录了,不是单纯的控制某一节点下的同一层了**。 |
| 90 | + |
| 91 | +如图: |
| 92 | + |
| 93 | + |
| 94 | + |
| 95 | +可以看出一旦把unordered_set<int> uset放在类成员位置,它控制的就是整棵树,包括树枝。 |
| 96 | + |
| 97 | +**所以这么写不行!** |
| 98 | + |
| 99 | +## 错误写法二 |
| 100 | + |
| 101 | +有同学把 unordered_set<int> uset; 放到类成员位置,然后每次进入单层的时候用uset.clear()。 |
| 102 | + |
| 103 | +代码如下: |
| 104 | + |
| 105 | +```C++ |
| 106 | +class Solution { |
| 107 | +private: |
| 108 | + vector<vector<int>> result; |
| 109 | + vector<int> path; |
| 110 | + unordered_set<int> uset; // 把uset定义放到类成员位置 |
| 111 | + void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) { |
| 112 | + result.push_back(path); |
| 113 | + uset.clear(); // 到每一层的时候,清空uset |
| 114 | + for (int i = startIndex; i < nums.size(); i++) { |
| 115 | + if (uset.find(nums[i]) != uset.end()) { |
| 116 | + continue; |
| 117 | + } |
| 118 | + uset.insert(nums[i]); // set记录元素 |
| 119 | + path.push_back(nums[i]); |
| 120 | + backtracking(nums, i + 1, used); |
| 121 | + path.pop_back(); |
| 122 | + } |
| 123 | + } |
| 124 | +``` |
| 125 | +uset已经是全局变量,本层的uset记录了一个元素,然后进入下一层之后这个uset(和上一层是同一个uset)就被清空了,也就是说,层与层之间的uset是同一个,那么就会相互影响。 |
| 126 | +
|
| 127 | +**所以这么写依然不行!** |
| 128 | +
|
| 129 | +**组合问题和排列问题,其实也可以使用set来对同一节点下本层去重,下面我都分别给出实现代码**。 |
| 130 | +
|
| 131 | +# 40. 组合总和 II |
| 132 | +
|
| 133 | +使用used数组去重版本:[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) |
| 134 | +
|
| 135 | +使用set去重的版本如下: |
| 136 | +
|
| 137 | +```C++ |
| 138 | +class Solution { |
| 139 | +private: |
| 140 | + vector<vector<int>> result; |
| 141 | + vector<int> path; |
| 142 | + void backtracking(vector<int>& candidates, int target, int sum, int startIndex) { |
| 143 | + if (sum == target) { |
| 144 | + result.push_back(path); |
| 145 | + return; |
| 146 | + } |
| 147 | + unordered_set<int> uset; // 控制某一节点下的同一层元素不能重复 |
| 148 | + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { |
| 149 | + if (uset.find(candidates[i]) != uset.end()) { |
| 150 | + continue; |
| 151 | + } |
| 152 | + uset.insert(candidates[i]); // 记录元素 |
| 153 | + sum += candidates[i]; |
| 154 | + path.push_back(candidates[i]); |
| 155 | + backtracking(candidates, target, sum, i + 1); |
| 156 | + sum -= candidates[i]; |
| 157 | + path.pop_back(); |
| 158 | + } |
| 159 | + } |
| 160 | +
|
| 161 | +public: |
| 162 | + vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { |
| 163 | + path.clear(); |
| 164 | + result.clear(); |
| 165 | + sort(candidates.begin(), candidates.end()); |
| 166 | + backtracking(candidates, target, 0, 0); |
| 167 | + return result; |
| 168 | + } |
| 169 | +}; |
| 170 | +``` |
| 171 | + |
| 172 | +# 47. 全排列 II |
| 173 | + |
| 174 | +使用used数组去重版本:[回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) |
| 175 | + |
| 176 | +使用set去重的版本如下: |
| 177 | + |
| 178 | +```C++ |
| 179 | +class Solution { |
| 180 | +private: |
| 181 | + vector<vector<int>> result; |
| 182 | + vector<int> path; |
| 183 | + void backtracking (vector<int>& nums, vector<bool>& used) { |
| 184 | + if (path.size() == nums.size()) { |
| 185 | + result.push_back(path); |
| 186 | + return; |
| 187 | + } |
| 188 | + unordered_set<int> uset; // 控制某一节点下的同一层元素不能重复 |
| 189 | + for (int i = 0; i < nums.size(); i++) { |
| 190 | + if (uset.find(nums[i]) != uset.end()) { |
| 191 | + continue; |
| 192 | + } |
| 193 | + if (used[i] == false) { |
| 194 | + uset.insert(nums[i]); // 记录元素 |
| 195 | + used[i] = true; |
| 196 | + path.push_back(nums[i]); |
| 197 | + backtracking(nums, used); |
| 198 | + path.pop_back(); |
| 199 | + used[i] = false; |
| 200 | + } |
| 201 | + } |
| 202 | + } |
| 203 | +public: |
| 204 | + vector<vector<int>> permuteUnique(vector<int>& nums) { |
| 205 | + result.clear(); |
| 206 | + path.clear(); |
| 207 | + sort(nums.begin(), nums.end()); // 排序 |
| 208 | + vector<bool> used(nums.size(), false); |
| 209 | + backtracking(nums, used); |
| 210 | + return result; |
| 211 | + } |
| 212 | +}; |
| 213 | +``` |
| 214 | +
|
| 215 | +# 两种写法的性能分析 |
| 216 | +
|
| 217 | +需要注意的是:**使用set去重的版本相对于used数组的版本效率都要低很多**,大家在leetcode上提交,能明显发现。 |
| 218 | +
|
| 219 | +原因在[回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。 |
| 220 | +
|
| 221 | +**而使用used数组在时间复杂度上几乎没有额外负担!** |
| 222 | +
|
| 223 | +**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。 |
| 224 | +
|
| 225 | +那有同学可能疑惑 用used数组也是占用O(n)的空间啊? |
| 226 | +
|
| 227 | +used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。 |
| 228 | +
|
| 229 | +# 总结 |
| 230 | +
|
| 231 | +本篇本打算是对[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)的一个点做一下纠正,没想到又写出来这么多! |
| 232 | +
|
| 233 | +**这个点都源于一位录友的疑问,然后我思考总结了一下,就写着这一篇,所以还是得多交流啊!** |
| 234 | +
|
| 235 | +如果大家对「代码随想录」文章有什么疑问,尽管打卡留言的时候提出来哈,或者在交流群里提问。 |
| 236 | +
|
| 237 | +**其实这就是相互学习的过程,交流一波之后都对题目理解的更深刻了,我如果发现文中有问题,都会在评论区或者下一篇文章中即时修正,保证不会给大家带跑偏!** |
| 238 | +
|
| 239 | +就酱,「代码随想录」一直都是干货满满,公众号里的一抹清流,值得推荐给身边的每一位同学朋友! |
| 240 | +
|
0 commit comments