#LC90. 子集II (Subsets II)

子集II (Subsets II)

labuladong讲解

你好,同学。欢迎来到 NOI 专题进阶课。在上两节课中,我们掌握了基础的“子集”生成。今天我们要挑战一个更具实战意义的变体——子集 II (Subsets II)

这道题的精髓在于处理重复元素。在 NOI 竞赛中,这属于“带限制条件的搜索”,也是掌握 搜索剪枝(Pruning) 逻辑的必经之路。


一、 题目描述 (NOI 风格)

题目名称:子集 II (Subsets II) 时间限制:1.0s 内存限制:256MB

【问题描述】 给定一个可能包含重复元素的整数序列 AA。请设计一个算法,输出该序列的所有可能子集(幂集)。 注意:

  1. 结果集中不能包含重复的子集。
  2. 你可以按任意顺序输出解集中的子集。
  3. 子集内部的元素顺序不作要求。

【输入格式】 输入共两行。 第一行包含一个整数 nn,表示序列中元素的个数。 第二行包含 nn 个以空格隔开的整数,表示序列 AA 中的元素。

【输出格式】 输出若干行,每行表示一个子集。元素之间用空格隔开。空集请保留一个空行。

【样例输入】

3
1 2 2

【样例输出】

(空行)
1
1 2
1 2 2
2
2 2

【数据范围】 对于 100%100\% 的数据:1n101 \le n \le 10,序列中的元素取值范围在 [10,10][-10, 10](注:虽然 LeetCode 原题范围较小,但掌握此方法后,n20n \le 20 的同类搜索题均可迎刃而解)


二、 预备知识点

  1. 回溯算法 (Backtracking):搜索所有可能的组合路径。
  2. 排序 (Sorting):这是处理重复元素的前提。只有将相同的数排在一起,我们才能有效地实施剪枝。
  3. 同层剪枝逻辑:理解搜索树中“同层”与“深层”的区别,这是避免产生重复子集的关键。

三、 启发式思路引导:草稿纸上的推理

假设集合是 {1, 2, 2}。如果不加限制地生成子集,你会得到两个 {1, 2},这在竞赛评测中会被判 WA。

1. 为什么会重复?

请在草稿纸上画出搜索树:

  • 第一层选 1
  • 第二层如果可选的有两个 2,如果你先选了“第一个 2”,再选“第二个 2”,或者跳过“第一个 2”直接选“第二个 2”,产生的路径都是 1 -> 2

2. 如何去重?(核心公式)

我们要保证在同一层递归中,同一个数字只被“作为开头”使用一次。

  • 前提:必须先对原数组进行 sort
  • 判断逻辑:在当前层的 for 循环中,如果 i > startnums[i] == nums[i-1],则说明“以这个数值为开头”的情况在当前层已经被处理过了,直接 continue

四、 算法流程图 (C++14 风格伪代码)

我们使用组合式 DFS 来实现,这是处理此类问题常数最小、逻辑最清晰的写法。

graph TD
    Start(开始) --> Sort(对 nums 进行从小到大排序)
    Sort --> Init(初始化 path 数组)
    Init --> CallDFS("DFS(起始索引 0)")

    subgraph DFS_Process [递归函数 DFS-start]
    Save(将当前 path 加入结果集或直接输出) --> LoopInit("循环 i 从 start 到 n-1")
    LoopInit --> CheckDup{"i 大于 start 且 nums(i) 等于 nums(i-1) ?"}
    
    CheckDup -- Yes --> NextI("跳过当前循环: continue")
    CheckDup -- No --> Action("path.push_back(nums(i))")
    
    Action --> Recur("递归进入下一层: DFS(i + 1)")
    Recur --> Pop("回溯恢复现场: path.pop_back")
    Pop --> NextI
    NextI --> LoopEnd{循环结束?}
    LoopEnd -- Yes --> Return((返回上一层))
    end

    Return --> End(结束)

五、 复杂度分析与优化建议

  • 时间复杂度分析

    • 总状态数最多为 2n2^n 个(当没有重复元素时)。
    • 排序耗时 O(nlogn)O(n \log n)
    • 每个状态构造耗时 O(n)O(n)
    • 总复杂度:O(n2n)O(n \cdot 2^n)
  • 空间复杂度分析

    • 递归深度 O(n)O(n)
    • path 数组占用 O(n)O(n)
  • 优化建议: 在 NOI 竞赛中,如果 nn 较大,可以使用位运算(Bitmask)配合 set<vector<int>> 进行去重,但效率通常不如上述的“排序+剪枝”高,因为剪枝直接在搜索过程中砍掉了整个分支。


六、 读题关键词总结

做 NOI 题目时,看到以下关键词要提高警惕:

  1. “可能包含重复元素” (Duplicates):这几乎是 100% 的信号告诉你需要先 sort
  2. “不能包含重复的子集” (Unique subsets):这是在提示你搜索逻辑中需要加入 i > start && nums[i] == nums[i-1] 的判断。
  3. “子集” (Power set):基本确定是深度优先搜索(DFS)或状态压缩。

教练寄语: 子集 II 是从“盲目搜索”向“聪明搜索”跨越的第一步。剪枝不仅能节省时间,更能保证逻辑的唯一性。一定要通过画搜索树,弄明白为什么是 i > start 而不是 i > 0!这是区分 OI 大牛和小白的关键细节。加油!