#90 回溯 中等

子集 II

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
两个 2 来自不同下标,[1,2] 与 [2,2] 各出现一次;不会出现两个相同的 [1,2]。

示例 2

输入:nums = [0]
输出:[[],[0]]

💭 模拟答题者思考

1. 我先照搬 #78「子集」:每层把当前 path 记入答案,用 start 只往后选——在 [1,2,2] 上会得到 [1,2] 两次(两个 2 各走一条分支),违反「解集不能重复」。

2. 重复在哪里?一是排列等价({1,2} 与 {2,1}),start 已解决;二是相同数值在同一层被多次当作「第一个新选元素」,生成相同子集(如两层都先选 2)。

3. 关键转化:先对 nums 排序;回溯框架与 #78 相同,但在 for i in range(start, n) 里加同层去重——若 i > start 且 nums[i] == nums[i-1] 则 continue(与 #40 组合总和 II 同款)。

4. 例 [1,2,2] 排序不变:收 [] → 选 1 收 [1] → 再选第一个 2 收 [1,2]、[1,2,2];同层第二个 2 作为「本层起点」被跳过,但可在更深层被选中,故仍有 [2]、[2,2]。

5. n≤10,回溯足够;位运算枚举掩码也能做,但重复元素下掩码去重更绕,排序 + DFS 更直观。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
pathlist<int>定义:当前正在构造的子集(按排序后下标递增选取的元素)
维护:DFS 在 path 末尾追加 nums[i],回溯时 pop 撤销
更新:进入一层递归前先记录当前 path;尝试分支时 append,返回后 pop
startint定义:本轮可选元素在排序后 nums 中的起始下标(含自身)
维护:只从 start 往后选,保证子集内下标单调递增,避免排列等价重复
更新:选了 nums[i] 后下一层传 start = i + 1
anslist<list<int>>定义:幂集中全部互不相同的子集
维护:每次进入 DFS 时把当前 path 的副本加入(与 #78 子集相同,每层都是合法状态)
更新:配合排序 + 同层去重,保证不会向 ans 写入重复子集

⌨️ 落码步骤

1. 对 nums 排序,初始化 ans 与 path

2. 定义 DFS backtrack(start):先将 path[:] 加入 ans

3. 对 i 从 start 到 len(nums)-1:若 i > start 且 nums[i] == nums[i-1],continue(同层去重)

4. 将 nums[i] 追加到 path,递归 backtrack(i + 1)

5. 回溯:从 path 弹出末尾,继续尝试更大的 i

6. 从 backtrack(0) 启动,返回 ans

💻 代码实现

class Solution:
    def subsetsWithDup(self, nums: list[int]) -> list[list[int]]:
        nums.sort()
        ans: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            ans.append(path[:])
            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i - 1]:
                    continue
                path.append(nums[i])
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return ans
class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        vector<int> path;

        function<void(int)> dfs = [&](int start) {
            ans.push_back(path);
            for (int i = start; i < (int)nums.size(); i++) {
                if (i > start && nums[i] == nums[i - 1]) continue;
                path.push_back(nums[i]);
                dfs(i + 1);
                path.pop_back();
            }
        };

        dfs(0);
        return ans;
    }
};
// 时间 O(n·2^n),空间 O(n) 递归栈

📈 复杂度分析

时间复杂度 O(n · 2^n)(最坏仍枚举全部不同子集;排序与同层去重减少重复输出)
空间复杂度 O(n)(递归栈深度与当前路径,不计输出)

⚠️ 常见坑

❗ 未排序就同层去重:nums[i]==nums[i-1] 只在相邻相等时有效,必须先 sort,否则重复值分散在不同位置无法跳过。

❗ 去重条件写成 i > 0:会误杀「不同层各选一个相同值」的合法子集(如 [1,1]);必须是 i > start,只跳过同一层的重复起点。

❗ 只在叶子收集:与 #78 相同,应在每层 DFS 入口记录 path,否则漏掉「不再往下选」的中间子集(如只含一个 2 的 [2])。

🔍 必测边界 Case

Case 1:全相同
nums = [2,2,2] → [[],[2],[2,2],[2,2,2]] 共 4 项
Case 2:示例 1
nums = [1,2,2] → 6 个子集,无重复
Case 3:单元素
nums = [0] → [[],[0]]
Case 4:空集必含
任意 nums → ans 第一项为 []
Case 5:负数与排序
nums = [-1,0,1] → 排序后回溯,子集个数仍为 2^n