leetcode经典子集问题

题目汇总

78. 子集:枚举不含重复元素的数组子集。

90. 子集 II:枚举含有重复元素数组的子数组,子数组中可以含重复元素但不能有重复子数组。解决办法见: 版本二

面试题 08.04. 幂集:枚举含重复元素的数组子集。子集中不能有重复元素,解决办法将:版本一

不含重复元素的子集枚举

二进制法

集合的每个元素,都有可以选或不选,用二进制和位运算,可以很好的表示。

每个子集对应一个掩码(将其看作0,1串),掩码的第i位表示nums[i]是否在子集中:

1——子集中包含当前元素

0——子集中不含当前元素

含有 n n n个元素的集合,一共有 2 n 2^n 2n个子集,掩码的最大值就是 2 n − 1 2^n-1 2n1 (0也是一个掩码) 。

	public List<List<Integer>> subsets(int[] nums) {
        int n = nums.length;
        List<List<Integer>> subsets = new ArrayList<>();
        for (int mask = 0; mask < (1 << nums.length); mask++) {
            List<Integer> subset = new ArrayList<>();
            for (int i = 0; i < nums.length; i++) {
                // mask的第i位表示nums[i]的取舍
                if (((mask >> i) & 1) == 1) { subset.add(nums[mask]); }
            }
            subsets.add(subset);
        }
        return subsets;
    }

时间复杂度: O ( n ⋅ 2 n ) O(n\cdot 2^n) O(n2n)

二叉树构造法

集合中每个元素的选和不选,构成了一个满二叉状态树,比如,左子树是不选,右子树是选,从根节点、到叶子节点的所有路径,构成了所有子集。

幂集:中序遍历.png

摘自leetcode78 题解
	public void getSubsets(int nums[], int lo, int hi, List<Integer> subset) {
        if (lo == hi) {
            subsets.add(new ArrayList<>(subset));
            return;
        }
        subset.add(nums[lo]);   // 取nums[lo]
        getSubsets(nums, lo + 1, hi, subset);
        subset.remove(subset.size() - 1);// 不取nums[lo]
        getSubsets(nums, lo + 1, hi, subset);
    }

时间复杂度 O ( 2 n ) O(2^n) O(2n),遍历完所有的节点。

含有重复元素的子集

以下三种方法复杂度较低。

回溯

幂集:回溯剪枝.png

摘自leetcode78 题解

效率最高!!

搜索之前需要排序

版本一:集合唯一
private List<List<Integer>> subsets = new ArrayList<>();
private void subsets(int[] nums, int lo, int hi, List<Integer> subset) {
    subsets.add(new ArrayList<>(subset));

    lable:
    for (int i = lo; i < hi; i++) {
        if (i != lo && nums[i] == nums[i - 1]) continue;// 搜索树中的同层存在重复元素,只考虑第一个重复元素nums[lo]
        subset.add(nums[i]);
        subsets(nums, i + 1, hi, subset);
        subset.remove(subset.size() - 1);
    }
}

public List<List<Integer>> subsetsWithDup(int[] nums) {
    subsets(nums, 0, nums.length, new ArrayList<>());
    return subsets;
}
版本二:集合元素唯一
private List<List<Integer>> subsets = new ArrayList<>();

private void subsets(int[] nums, int lo, int hi, List<Integer> subset) {
    subsets.add(new ArrayList<>(subset));

    lable:
    for (int i = lo; i < hi; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;	// 重复元素不在考虑
        subset.add(nums[i]);
        subsets(nums, i + 1, hi, subset);
        subset.remove(subset.size() - 1);
    }
}

public List<List<Integer>> subsetsWithDup(int[] nums) {
    subsets(nums, 0, nums.length, new ArrayList<>());
    return subsets;
}

subsetsWithDup(new int[] {1,1,2}) :

[]
[1]
[1, 2]
[2]

根据幂集的性质

设集合A的幂集是 P ( A ) = { A i ∣ A i ⊂ A } P(A)=\{A_i|A_i\subset A\} P(A)={AiAiA},即 P ( A ) P(A) P(A)的每一个元素都是集合 A A A的子集;那么集合 A ∪ a A\cup a Aa 的幂集是 P ( A ) ∪ { A i ∪ a ∣ A i ∈ P ( A ) } P(A)\cup \{A_i\cup a| A_i\in P(A)\} P(A){AiaAiP(A)}

版本一:集合元素唯一

可以保证集合元素唯一,但是不能保证集合唯一!!除非用HashSet<List>,阿这!!

为了去重需要对输入数组排序!

public List<List<Integer>> getSubsets(int nums[], int lo, int hi) {
    Arrays.sort(nums, lo, hi);	// 为了处理重复元素,需要先排序,令重复元素位于相邻位置。
    
    List<List<Integer>> subsets = new ArrayList<>();
    subsets.add(new ArrayList<>()); // 空集是任何集合的子集

    lable:
    for (int i = lo; i < hi; i++) {
        
        int size = subsets.size();
        for (int j = 0; j < size; j++) {
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            List<Integer> subset = new ArrayList<>(subsets.get(j));
            subset.add(nums[i]);
            subsets.add(subset);
        }
    }
    return subsets;
}
getSubsets(new int[]{1, 1, 2, 3}, 0, 4)

[]
[1]
[2]
[1, 2]
[3]
[1, 3]
[2, 3]
[1, 2, 3]

枚举子集的规模

版本一:集合元素唯一

长度为n的数组,其子集的长度为0-n,外层循环枚举子集规模size,内存循环通过回溯搜索长度为size的子集即可。

处理重复元素:在递归之前,跳过重复元素。

为了处理重复元素,需要先对输入数组排序

private List<List<Integer>> subsets = new ArrayList<>();

/**
 * 搜索nums[lo..hi]中长度为n的子集
 */
private void getSubsets(int[] nums, int lo, int hi, int n, List<Integer> subset) {
    if (subset.size() == n) {subsets.add(new ArrayList<>(subset));return;}  

    for (int i = lo; i < hi; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;	// 跳过已经处理过的重复元素
        subset.add(nums[i]);
        getSubsets(nums, i + 1, hi, n, subset);
        subset.remove(subset.size() - 1);
    }
}

public void getSubsets(int nums[], int lo, int hi) {
	Arrays.sort(nums, lo, hi);
    int n = hi - lo;
    for (int size = 0; size <= n; size++) {
        // 枚举子集的规模
        getSubsets(nums, lo, hi, size, new ArrayList<>());
    }
}
getSubsets(new int[]{1, 1, 2, 3}, 0, 4);

[]
[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]

版本二:集合唯一

版本二与版本一的区别是,版本二允许一个集合中存在重复元素,但是与版本一相同的是均不存在重复集合。

解决办法:递归树的同一层存在重复元素,只考虑第一个元素。

注意“去重”操作是在同层中进行的,不同层的相同元素不能“去重”。
比如nums=[1,2,2],根节点有三个子节点,即1,2和2,
当for循环到该层的最后一个子节点,即重复出现的那个2的时候,需要及时掐断,不再向下递归;

private List<List<Integer>> subsets = new ArrayList<>();

private void subsets(int[] nums, List<Integer> subset, int n, int lo) {
    if (subset.size() == n) {
        subsets.add(new ArrayList<>(subset));
        return;
    }

    lable:
    for (int i = lo; i < nums.length; i++) {
        if (i != lo && nums[i] == nums[i - 1]) continue;// 搜索树中的同层存在重复元素,只考虑第一个重复元素nums[lo]
        subset.add(nums[i]);
        subsets(nums, subset, n, i + 1);
        subset.remove(subset.size() - 1);
    }
}

public List<List<Integer>> subsets(int[] nums) {
    for (int size = 0; size <= nums.length; size++) {
        subsets(nums, new ArrayList<>(), size, 0);
    }
    return subsets;
}

注意:搜索子集之前要先排序。

solution.subsets(new int[]{1, 2, 2});

[]
[1]
[2]
[1, 2]
[2, 2]
[1, 2, 2]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值