#LC78. 子集
子集
labuladong的讲解:回溯算法秒杀所有排列/组合/子集问题
你好,同学。作为你的教练,今天我们来深入剖析一道非常经典的组合搜索基础题——子集(Subsets)。在 NOI 竞赛中,这类题目是构建搜索剪枝、状态压缩动态规划以及组合数学的基础。
一、 题目描述 (NOI 风格)
题目名称:子集 (Subsets) 时间限制:1.0s 内存限制:256MB
【问题描述】 给定一个包含 个不重复正整数的集合 。请设计一个算法,输出该集合的所有可能子集(幂集)。 注意:输出的子集中,元素的顺序不作要求,子集之间的顺序也不作要求。
【输入格式】 输入共两行。 第一行包含一个整数 ,表示集合中元素的个数。 第二行包含 个以空格隔开的正整数,表示集合 中的元素。
【输出格式】 输出若干行,每行代表一个子集。元素之间用空格隔开。空集请保留一个空行或以特殊标记处理(在本题中,直接输出所有组合即可)。
由于子集的遍历顺序可能不一样,可能有多种结果。本题测评使用了SPJ程序测评,只要求任意一种满足要求的结果就行
【样例输入】
3
1 2 3
【样例输出】
(空行)
3
2
2 3
1
1 3
1 2
1 2 3
【数据范围】
对于 的数据:,集合中的元素互不相同,且每个元素均在 int 范围内。
二、 预备知识点
- 递归与回溯 (Recursion & Backtracking):理解搜索树的生成过程。
- 位运算 (Bit Manipulation):利用二进制状态压缩表示集合。
- 组合数学:长度为 的集合,其子集总数为 。
- STL 容器:
std::vector的基本操作。
三、 启发式思路引导:草稿纸上的推理
现在,请拿出你的草稿纸,我们不直接写代码,而是画出逻辑。
1. 决策树法(启发式思考)
假设集合是 {1, 2, 3}。对于每一个元素,你在构建子集时只有两种选择:选 或 不选。
- 第 1 步:考察元素
1。- 分支 A:放入子集。
- 分支 B:不放入子集。
- 第 2 步:在上述两个分支的基础上,考察元素
2。- 分支 A 派生出:
{1, 2}和{1}。 - 分支 B 派生出:
{2}和{}。
- 分支 A 派生出:
- 第 3 步:以此类推考察元素
3。
思考:你会发现这构成了一棵深度为 的满二叉树。叶子节点的总数正好是 。
2. 状态压缩法(图形化思考)
如果你把“选”记作 1,“不选”记作 0:
- 子集
{1, 3}对应二进制101(十进制 5)。 - 子集
{2}对应二进制010(十进制 2)。 - 所有子集正好对应整数范围 里的每一个数字。
四、 复杂度分析与优化建议
-
时间复杂度分析:
- 总共有 个状态。
- 每个状态需要 的时间构造或输出。
- 总复杂度:。
- 优化建议:在 较小时,递归和位运算效率差异不大;当 时,单纯的子集枚举将超时,需考虑剪枝或动态规划。
-
空间复杂度分析:
- 递归深度为 。
- 如果需要存储所有结果,空间为 。在 NOI 中,通常建议边搜边输出以节省内存。
五、 算法流程图 (C++14 风格伪代码)
我们使用回溯法来描述这个过程,这是最符合 OI 搜索题习惯的写法。
graph TD
Start(开始) --> Init(初始化 path 数组和结果集)
Init --> CallDFS("DFS(0, 初始集合)")
subgraph DFS_Process [递归搜索函数]
NodeCheck{当前索引 u == n ?}
NodeCheck -- Yes --> Save(保存当前 path 并返回)
NodeCheck -- No --> Choice1("决策1: 不选 nums(u)")
Choice1 --> Recur1("DFS(u + 1)")
Recur1 --> Choice2("决策2: 选 nums(u)")
Choice2 --> Push("path.push_back(nums(u))")
Push --> Recur2("DFS(u + 1)")
Recur2 --> Pop("回溯: path.pop_back()")
end
Save --> End(结束)
Pop --> Return((返回上一层))
六、 读题关键词总结
在做 NOI 搜索类题目时,看到以下关键词要形成条件反射:
- “所有可能的” (All possible):暗示这可能是一道搜索题(DFS/BFS)或者动态规划题,需要遍历整个解空间。
- “互不相同/不重复” (Distinct/Unique):这是好消息,说明你不需要处理“去重”逻辑(如
if (i > start && nums[i] == nums[i-1]) continue)。 - “子集” (Subsets/Power Set):立刻想到解的数量是 。检查 的范围,如果 ,可以直接暴力搜索;如果 很大,必然有贪心、剪枝或数学规律。
- “不限顺序” (In any order):意味着你不需要在搜索过程中进行复杂的排序处理来保证输出顺序。
教练寄语: 子集问题是搜索的灵魂。理解了“选与不选”的决策树,你就开启了通往深度优先搜索(DFS)的大门。尝试用位运算和递归两种方式都实现一遍,这会对你理解计算机底层逻辑非常有帮助!加油!