当前:LC90 · 子集 II · 首次出现于 Day 28 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC90 · Subsets II · 回溯 / 去重

子集 II:同一层的第二个相同值不要再开叉

先排序让相同数挨在一起。for 里若这个值和前一个一样、且不是本层第一个,跳过——重复子集就不会再长出来。

与 LC78 相同,但先排序。在 for 循环中,若 nums[i] == nums[i−1] 且 i > idx,跳过——即同一决策层的重复值不再分叉。这样每个子集只被生成一次。时间 O(n·2^n),空间 O(n)。

时间 O(n·2^n)空间 O(n)结论先行 · 全文约 6 节
导读

给定可能含重复数字的数组,返回所有不重复的子集。子集里元素的顺序不重要,[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]。
nums=[1,2,2]
122
当前路径[1]
{}{1}
层 1 候选 [2,2]:i=1 是第一个 2,选

Go:排序 + 同层去重

solution.goGo
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,那是另一张树。
同族题目
LC78子集LC40组合总和 IILC47全排列 II