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