回溯算法——子集问题

子集问题

一、问题特点

子集问题要求从指定的集合中找出所有满足特定条件的子集,当使用回溯算法解决子集问题的时候,相当于要获得搜索树上所有满足条件节点的值(在组合和切割问题中只求叶子结点的值)

子集问题有以下注意事项:

  • 因为子集中元素是无序的,所以也需要用startIndex去重
  • 将子集添加到结果数组中的操作应当在每一次回溯函数调用时都被执行
  • 子集问题的搜索方式和组合问题大体相同,当原集合中存在重复元素时,也需要进行树层去重
  • 当子集中不能有重复元素时,需要进行树枝去重

二、相关例题

(一)子集

78.子集(LeetCode)
问题描述:

给你一个整数数组 nums ,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。

示例:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

提示:

1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同

解题思路: 和组合问题基本相同,只需要调整将元素加入路径的条件使路径上的所有元素均可以被加入路径即可,此处不再赘述。(组合方法详情参考《回溯算法及其应用》

代码示例:

class Solution {
public:
    void backtracking(vector<int> nums, int startIndex) {
        // 因为需要求幂集,需要将每一个集合都加入结果中
        // 首次调用函数加入空集
        ans.push_back(path);
        if(startIndex == nums.size()) return;
        for(int i = startIndex; i < nums.size(); ++i) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
        return;
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return ans;
    }
private:
    vector<int> path;
    vector<vector<int>> ans;
};

(二)子集II

90.子集II(LeetCode)
问题描述:

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。

示例:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

提示:

1 <= nums.length <= 10
-10 <= nums[i] <= 10

解题思路:组合总和II去重思路相同,注意原集合中可能有重复元素,且每个元素在同一子集中只能取一次(不用做树枝去重)。但由于集合的无序性,需要避免在同一层重复取具有相同值的元素(做树层去重)。

代码示例:

class Solution {
public:
    void backtracking(vector<int> nums, vector<bool>& visited, int startIndex) {
        ans.push_back(path);
        if(startIndex >= nums.size()) return;
        for(int i = startIndex; i < nums.size(); ++i) {
            // 如果visited[i-1] = true说明nums[i-1]被上一层取过,本层仍可使用
            if(i > 0 && nums[i-1] == nums[i] && visited[i-1] == false) {
                continue;
            }
            path.push_back(nums[i]);
            visited[i] = true;
            backtracking(nums, visited, i + 1);
            path.pop_back();
            visited[i] = false;
        }
        return;
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        // 需要进行树层去重
        // 根据原集合的大小初始化visited数组
        vector<bool> visited(nums.size(), false);
        // 保证相同值的元素相邻
        sort(nums.begin(), nums.end());
        backtracking(nums, visited, 0);
        return ans;
    }
private:
    vector<int> path;
    vector<vector<int>> ans;
};

(三)递增子序列

491.递增子序列(LeetCode)
问题描述:

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素。你可以按任意顺序返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例:

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

提示:

1 <= nums.length <= 15
-100 <= nums[i] <= 100

解题思路:
本题同样需要找出所有满足条件的子集,且原数组中也有可能有重复元素,需要进行树层去重,但注意本题中隐含了一个陷阱:题目要求返回递增子序列,也就是说子序列中各元素的相对位置必须保持不变,在查找以前不能对原数组进行排序,这也就意味着上一题中使用visited数组去重的办法没有办法使用了。
为了进行树层遍历,还可以建立一个无序set,该表在回溯函数内建立并完成初始化,每一层中有且只有一个无序set,用于记录本层已经使用过的节点,每次找到不小于路径中最大节点的节点以后只需查找表即可获知本层是否使用过该元素,从而完成剪枝的判断。
代码示例:

class Solution {
public:
    void backtracking(vector<int> nums, int startIndex) {
        // 用无序set表示
        if(path.size() > 1) res.push_back(path);
        if(startIndex >= nums.size()) return;
        unordered_set<int> map;
        // map在每次函数调用时更新,只能记录同一层的元素,隐含回溯
        for(int i = startIndex; i < nums.size(); ++i) {
            // 如果当前元素的值小于路径中的最大值,加入后子集将不再是递增序列,直接剪枝
            if(!path.empty() && path.back() > nums[i]) continue;
            // 如果在map中发现元素,说明是同层元素,需要剪枝
            if(map.find(nums[i]) != map.end()) continue;
            map.insert(nums[i]);
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
        return;
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        path.clear();
        res.clear();
        backtracking(nums, 0);
        return res; 
    }
private:
    vector<int> path;
    vector<vector<int>> res;
};
  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值