#LC90. 子集II (Subsets II)
子集II (Subsets II)
你好,同学。欢迎来到 NOI 专题进阶课。在上两节课中,我们掌握了基础的“子集”生成。今天我们要挑战一个更具实战意义的变体——子集 II (Subsets II)。
这道题的精髓在于处理重复元素。在 NOI 竞赛中,这属于“带限制条件的搜索”,也是掌握 搜索剪枝(Pruning) 逻辑的必经之路。
一、 题目描述 (NOI 风格)
题目名称:子集 II (Subsets II) 时间限制:1.0s 内存限制:256MB
【问题描述】 给定一个可能包含重复元素的整数序列 。请设计一个算法,输出该序列的所有可能子集(幂集)。 注意:
- 结果集中不能包含重复的子集。
- 你可以按任意顺序输出解集中的子集。
- 子集内部的元素顺序不作要求。
【输入格式】 输入共两行。 第一行包含一个整数 ,表示序列中元素的个数。 第二行包含 个以空格隔开的整数,表示序列 中的元素。
【输出格式】 输出若干行,每行表示一个子集。元素之间用空格隔开。空集请保留一个空行。
【样例输入】
3
1 2 2
【样例输出】
(空行)
1
1 2
1 2 2
2
2 2
【数据范围】 对于 的数据:,序列中的元素取值范围在 。 (注:虽然 LeetCode 原题范围较小,但掌握此方法后, 的同类搜索题均可迎刃而解)
二、 预备知识点
- 回溯算法 (Backtracking):搜索所有可能的组合路径。
- 排序 (Sorting):这是处理重复元素的前提。只有将相同的数排在一起,我们才能有效地实施剪枝。
- 同层剪枝逻辑:理解搜索树中“同层”与“深层”的区别,这是避免产生重复子集的关键。
三、 启发式思路引导:草稿纸上的推理
假设集合是 {1, 2, 2}。如果不加限制地生成子集,你会得到两个 {1, 2},这在竞赛评测中会被判 WA。
1. 为什么会重复?
请在草稿纸上画出搜索树:
- 第一层选
1。 - 第二层如果可选的有两个
2,如果你先选了“第一个 2”,再选“第二个 2”,或者跳过“第一个 2”直接选“第二个 2”,产生的路径都是1 -> 2。
2. 如何去重?(核心公式)
我们要保证在同一层递归中,同一个数字只被“作为开头”使用一次。
- 前提:必须先对原数组进行
sort。 - 判断逻辑:在当前层的
for循环中,如果i > start且nums[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(结束)
五、 复杂度分析与优化建议
-
时间复杂度分析:
- 总状态数最多为 个(当没有重复元素时)。
- 排序耗时 。
- 每个状态构造耗时 。
- 总复杂度:。
-
空间复杂度分析:
- 递归深度 。
path数组占用 。
-
优化建议: 在 NOI 竞赛中,如果 较大,可以使用位运算(Bitmask)配合
set<vector<int>>进行去重,但效率通常不如上述的“排序+剪枝”高,因为剪枝直接在搜索过程中砍掉了整个分支。
六、 读题关键词总结
做 NOI 题目时,看到以下关键词要提高警惕:
- “可能包含重复元素” (Duplicates):这几乎是 100% 的信号告诉你需要先
sort。 - “不能包含重复的子集” (Unique subsets):这是在提示你搜索逻辑中需要加入
i > start && nums[i] == nums[i-1]的判断。 - “子集” (Power set):基本确定是深度优先搜索(DFS)或状态压缩。
教练寄语:
子集 II 是从“盲目搜索”向“聪明搜索”跨越的第一步。剪枝不仅能节省时间,更能保证逻辑的唯一性。一定要通过画搜索树,弄明白为什么是 i > start 而不是 i > 0!这是区分 OI 大牛和小白的关键细节。加油!