子集 II:同一层的第二个相同值不要再开叉
先排序让相同数挨在一起。for 里若这个值和前一个一样、且不是本层第一个,跳过——重复子集就不会再长出来。
与 LC78 相同,但先排序。在 for 循环中,若 nums[i] == nums[i−1] 且 i > idx,跳过——即同一决策层的重复值不再分叉。这样每个子集只被生成一次。时间 O(n·2^n),空间 O(n)。
给定可能含重复数字的数组,返回所有不重复的子集。子集里元素的顺序不重要,[1,2] 和 [2,1] 算同一个;空集要留下。
主例 nums = [1, 2, 2]。不重复的 6 个是 []、[1]、[2]、[1,2]、[2,2]、[1,2,2]。若直接套 LC78,两个 2 会被当成不同选择,[2] 和 [1,2] 各出现两次。演示在 path=[1] 的那一层选了第一个 2,再遇到第二个 2 就跳过。
用集合去重也能交,但 2^n 个子集都先造出来再扔,浪费且要处理排序后的比较。本文要回答:为什么「同层跳过」不会误删 [2,2],以及主例那一次 continue 砍掉的是哪条重复枝。
缺口是同层分叉,不是路径上的第二个 2
LC78 对每个下标只问选或不选,数字都不同时不会撞车。主例多了一个 2。从下标 1 选 2 得到 [2];从下标 2 选另一个 2 也得到 [2]。两条枝的内容一样,答案集脏了。缺的不是「能不能选两个 2」——[2,2] 合法——而是同一层里,相同的值只能当一次「本层的新选择」。
先排序,相同值相邻,才能用「和前一个比」发现重复。回溯仍是 for i := idx; i < n; i++:本层从 idx 起枚举「这一个位置选谁」。若 i > idx 且 nums[i]==nums[i-1],说明本层刚刚已经用同样的值开过一条枝,这条再开会得到相同子集,continue。i==idx 是本层第一次碰到这个值,必须走。
不同深度不受这句限制。选了下标 1 的 2 之后,下一层 idx 变成 2,i==idx 的那个 2 是新一层的第一次,可以再选,于是 [2,2]、[1,2,2] 都在。被跳过的只是「同一层的第二个 2」,不是「路径里不能出现两个 2」。
用手走主例。排序后仍是 [1,2,2]。dfs(0) 先收下 []。选 1,进入 dfs(1),path=[1],收下 [1]。本层 idx=1,i=1 是第一个 2,选上,path=[1,2],演示第一帧。回到 [1] 后 i=2:i>1 且两个 2 相同,跳过,演示第二帧 skipped=2。这条被砍掉的枝本来会再长出一个 [1,2],与刚才重复。
回到空 path。i=1 选第一个 2,收下 [2],再往下选第二个 2,收下 [2,2]。i=2 在这一层同样因「同层重复」跳过,避免第二个单独的 [2]。六份收齐,与演示终帧一致。收集时必须拷贝 path,后面的 pop 会改共享切片。
不排序就无法用相邻比较。used 数组去重是排列题的做法,这里下标本身已经单向递增,不会回头选,只要同层去重。LC40 组合总和 II 用的是同一句 i>idx && 值相同。
树层去重i > idx 表示「不是本层第一个候选」。同层第二个 2 跳过;下一层的 2 是新的 idx,可以选。主例因此既有 [2,2],又没有两份 [2]。
Go:排序 + 同层去重
func subsetsWithDup(nums []int) [][]int {sort.Ints(nums)res := [][]int{}path := []int{}var dfs func(int)dfs = func(idx int) {res = append(res, append([]int{}, path...))for i := idx; i < len(nums); i++ {if i > idx && nums[i] == nums[i-1] { continue }path = append(path, nums[i])dfs(i + 1)path = path[:len(path)-1]}}dfs(0)return res}
1先排序。不排序,nums[i]==nums[i-1] 没有意义。
2进入每一层先拷贝 path 入结果,空集也在第一次 dfs(0) 收下。
3i>idx 且值相同则跳过。主例 path=[1] 时第二个 2 走这句。
4dfs(i+1) 单向前进,同一元素不会被选两次下标;pop 恢复给同层下一个候选。
总结
排序后同层跳过相同值。[1,2,2] 收 6 个子集,没有两份 [2]。
- 重复子集来自同一层的第二次相同选择,不是来自路径上的两个 2。
- i>idx 是「同层」判据。i==idx 的第一次必须走。
- LC40 组合去重是同一句。排列去重还要 used,那是另一张树。