含重复元素取不重复子集[如何取子集?如何去重?]

前言

发现关键问题是一个人的核心能力,就比如含重复元素取不重复子集,两个关键问题就是,第一,如何取子集?第二,如何去重?
针对如何取子集,该文整理了三种方式;
针对如何去重,该文整理了两种方式。

一、子集II

在这里插入图片描述

二、取子集+去重

package everyday.bitManipulation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SubsetsWithDup {
    /*
    target:不重复的子集。
    如何取子集?不断新增一个元素。
    如何去重?排序+pre+记录最后被添加的一组的位置。
     */
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        
        List<List<Integer>> subsets = new ArrayList<>();
        subsets.add(new ArrayList<>());
        
        int pre = 1 << 31, begin = 0;
        for (int num : nums) {
            int i = 0, size = subsets.size();
            
            if (pre == num) i = begin;

            while (i < size) {
                List<Integer> el = new ArrayList<>(subsets.get(i++));
                el.add(num);
                subsets.add(el);
            }
            begin = size;
            pre = num;
        }
        return subsets;
    }

    /*
    上面我们已经挖掘了两个核心问题,而且采用自己的理解方式完成了题解。
    看一哈官方是怎么搞的高级做法,即官方是如果解决如下两个问题的,
    1-如何取子集?二进制枚举法 + 取该位为1的集合元素,时间复杂度O(2^n^ * n)
    2-如何去重?排序 + 前后两者相等 + 前一位在子集中
     */
    public List<List<Integer>> subsetsWithDup2(int[] nums) {
        int n = nums.length;

        Arrays.sort(nums);

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

        for (int mask = 0; mask < 1 << n; mask++) {
            // 通过二进制的每一位来取元素。
            boolean isValid = true;
            for (int i = 0; i < n; i++) {
                // 判定该元素是否在当前子集中。
                if ((mask & 1 << i) != 0) {
                    if (i > 0 && nums[i] == nums[i - 1] && (mask >> i - 1 & 1) == 0) {
                        // 出现重复
                        isValid = false;
                        break;
                    }
                    // 没有重复
                    subset.add(nums[i]);
                }
            }
            // 重复则不要该重复且半截截子集,不重复就要。
            if (isValid) subsets.add(new ArrayList<>(subset));
            // 清空子集
            subset.clear();
        }
        return subsets;
    }

    /*
    上面我们已经挖掘了两个核心问题,而且采用自己的理解方式完成了题解。
    看一哈官方是怎么搞的高级做法,即官方是如果解决如下两个问题的,
    1-如何取子集?选择 or 不选择当前元素。
    2-如何去重?排序+上一个和当前选择的元素没有同时出现 -> 越过此子集。
     */
    public List<List<Integer>> subsetsWithDup3(int[] nums) {
        int n = nums.length;

        Arrays.sort(nums);

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

        dfs(nums, 0, false, subset, subsets);

        return subsets;
    }

    // 选 or 不选,共考虑n个元素,所以是(1 + 1)^n^
    private void dfs(int[] nums, int cur, boolean isChoosePre, List<Integer> subset, List<List<Integer>> subsets) {
        if (cur == nums.length) {
            subsets.add(new ArrayList<>(subset));
            return;
        }
        // 选择 or 不选择当前元素。
        // 不选择当前元素,屁事没有
        dfs(nums, cur + 1, false, subset, subsets);
        // 选择当前元素,当cur - 1却没选择,且两者相等,就必会出现重复子集,越过。
        if (cur > 0 && nums[cur] == nums[cur - 1] && !isChoosePre) return;
        // 前面的选了 or 前后不相等,正常添加元素,生成子集。
        // 标准回溯
        subset.add(nums[cur]);
        dfs(nums, cur + 1, true, subset, subsets);
        subset.remove(subset.size() - 1);
    }
}

总结

1)锻炼自己发现核心问题/提出关键问题的能力。
2)子集的三种生成方式,循环加元素 | 二进制枚举 | dfs选或不选元素
3)子集重复的两种判定方式,排序+前后是否相等 | 排序+前后是否相等+前面是否被选

参考文献

[1] LeetCode 子集II

递归生子集时,为了避免生重复子集,可以采用以下策略: 1. **排序**:首先对原始数组进行排序,这样可以确保在递归过程中,相同的元素是连续的,从而便于在生子集时避免重复。 2. **使用标记数组**:可以使用一个布尔数组(标记数组)来记录哪些元素已经被选择加入当前子集,这样在递归的每一层都可以根据这个标记数组来决定是否将某个元素加入子集。 3. **控制递归**:在递归函数中加入判断条件,比如当尝试加入的元素与前一个元素相同时,只有当它未被标记时才加入子集,这样可以避免重复。 下面是一个简单的递归生子集的伪代码示例,其中包了避免生重复子集的逻辑: ``` function generateSubsets(arr, index, currentSubset, allSubsets, used) { if (index == arr.length) { allSubsets.add(new ArrayList(currentSubset)); // 将当前子集添加到结果集合中 return; } // 不包当前元素 generateSubsets(arr, index + 1, currentSubset, allSubsets, used); // 包当前元素,但需确保当前元素与前一个元素不同或前一个元素未被使用 if (index > 0 && arr[index] == arr[index - 1] && !used[index - 1]) { return; // 避免重复 } used[index] = true; // 标记当前元素已使用 currentSubset.add(arr[index]); // 将当前元素加入子集 generateSubsets(arr, index + 1, currentSubset, allSubsets, used); // 继续递归 currentSubset.remove(currentSubset.size() - 1); // 回溯,移除当前元素 used[index] = false; // 消标记 } // 调用示例 ArrayList<ArrayList<Integer>> allSubsets = new ArrayList<>(); boolean[] used = new boolean[arr.length]; // 用于标记是否使用了某个元素 Arrays.sort(arr); // 对数组进行排序 generateSubsets(arr, 0, new ArrayList<>(), allSubsets, used); ``` 在这个示例中,`arr`是输入数组,`allSubsets`是存储所有子集集合,`currentSubset`是当前正在构建的子集,`used`是标记数组,用于记录哪些元素已经被用于构建子集
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值