回溯算法-子集问题-子集II

leetcode 90.子集II

class Solution {
private: 
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(vector<int>& nums,int startIndex,vector<bool>& used){//1.确定函数的参数和返回值
    //2.确定终止条件
    result.push_back(path);
    if(startIndex==nums.size()) return ;
    //确定单层逻辑
    for(int i=startIndex;i<nums.size();i++){
        if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
            continue;
        }else{
            used[i]=true;// 处理
            path.push_back(nums[i]);//处理节点
            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;
    }
};

注意:

  1. 子集问题中树的每个节点都要收集,因此每次递归不需要if判断,直接push进result。
  2. result.push的时候不需要判断path是否合法,因为在生成path的时候已经判断了这个path是否合法,即每个往path里面加的数据都经过了if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){ continue;的判断了。
  3. for循环里如果一个数字不合法,那么continue,而不是break,因为那一层也许只有那一个数字不合法,但不代表后面的数字不合法,直接跳过当前数字continue下一个数字即可,而IP复原问题中一个数字不合法代表那一条支路全部不合法,不需要再向下递归了,直接break回到上一层遍历别的支路。
  4. 另一种去重方法 不使用used数组去重,而是使用startIndex,这个理解起来很抽象,就是如果i>startIndex,但是nums[i]=nums[i-1]说明nums[i-1]必定在那一层的前面已经遍历过了,因此当前的nums[i]就不用遍历了,直接continue到下一个元素即可
    for(int i=startIndex;i<nums.size();i++){ path.push_back(nums[i]);//3.1处理 backtracking(nums,i+1);//3.2递归 path.pop_back();//3.3回溯 }
  5. 还可以使用set去重,但依旧需要对数组sort排序,原理可以自己画个图 以 212 数组为例,如果不排序 212 那么在1节点下收集结果的时候会重复收集12 ,但是如果排序了后,通过i=startIndex可以自动帮我们去掉这个重复的结果12。set去重的代码如下
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) {
        result.push_back(path);
        unordered_set<int> uset; // 定义set对同一节点下的本层去重
        for (int i = startIndex; i < nums.size(); i++) {
            if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass
                continue;
            }
            uset.insert(nums[i]); // set跟新元素
            path.push_back(nums[i]);
            backtracking(nums, i + 1, used);
            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;
    }
};

需要注意的是:使用set去重的版本相对于used数组的版本效率都要低很多,大家在leetcode上提交,能明显发现。

原因在回溯算法:递增子序列 (opens new window)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。

而使用used数组在时间复杂度上几乎没有额外负担!

使用set去重,不仅时间复杂度高了,空间复杂度也高了,在本周小结!(回溯算法系列三) (opens new window)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。

那有同学可能疑惑 用used数组也是占用O(n)的空间啊?

used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值