阅读量:2
概述
在利用回溯算法去求子集、排列、组合等问题时,所给数组中如果包含重复元素,需要进行去重操作。常用的去重方法是使用used数组或集合。
对于上述子集、排列、组合等问题的求解方法是将数组转化为树的形式,利用递归和回溯的方法进行求解。去重操作主要处理的是在同一层的数据的重复操作。
例如:力扣90题子集问题,将其转换为树的形式以及去重操作示意图如下
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
used方法为设置一个bool类型的数组,数组中元素如果为false则表示在同一层使用过,如果为true表示在同一树枝(纵向)使用过。如果用set集合的方式,则如果索引到的元素在集合出现过,则表示在同一层使用过。两者不同的是used数组在函数参数定义及函数外定义,所占空间内存小,为O(n),set集合在函数中for循环外定义,每次递归需要再次展开内存,所占空间大,为O(n*2)。
例题
集合
数组形式,注意要进行排序
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) { result.push_back(path); for (int i = startIndex; i < nums.size(); i++) { // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 // 而我们要对同一树层使用过的元素进行跳过 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; } path.push_back(nums[i]); used[i] = true; backtracking(nums, i + 1, used); used[i] = false; path.pop_back(); } } public: vector<vector<int>> subsetsWithDup(vector<int>& nums) { result.clear(); path.clear(); vector<bool> used(nums.size(), false); sort(nums.begin(), nums.end()); // 去重需要排序 backtracking(nums, 0, used); return result; } };
集合形式
void trversal(vector<int>&nums,int index) { result.push_back(path); if(index>=nums.size()) return; unordered_set<int>uset; for(int i=index;i<nums.size();i++) { if(uset.find(nums[i])!=uset.end())//去重,同一层 continue; path.push_back(nums[i]); uset.insert(nums[i]); trversal(nums,i+1); path.pop_back(); } } vector<vector<int>> subsetsWithDup(vector<int>& nums) { result.clear(); path.clear(); // vector<bool>used(nums.size(),false); sort(nums.begin(),nums.end()); trversal(nums,0); return result; } };
全排列
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
- 1 <= nums.length <= 8
- -10 <= nums[i] <= 10
对于排列问题,和集合问题的不同点在于for循环的开始位置不同,集合问题为当前元素的下一个元素的位置,而排列问题一直为数组的起始位置,需要注意的是要加上元素是否使用的判断。
代码如下
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking (vector<int>& nums, vector<bool>& 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) {//未在树枝使用,可以加入到path数组中 used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } } } public: vector<vector<int>> permuteUnique(vector<int>& nums) { result.clear(); path.clear(); sort(nums.begin(), nums.end()); // 排序 vector<bool> used(nums.size(), false); backtracking(nums, used); return result; } }; // 时间复杂度: 最差情况所有元素都是唯一的。复杂度和全排列1都是 O(n! * n) 对于 n 个元素一共有 n! 中排列方案。而对于每一个答案,我们需要 O(n) 去复制最终放到 result 数组 // 空间复杂度: O(n) 回溯树的深度取决于我们有多少个元素