#LC78. 子集

子集

labuladong的讲解:回溯算法秒杀所有排列/组合/子集问题

你好,同学。作为你的教练,今天我们来深入剖析一道非常经典的组合搜索基础题——子集(Subsets)。在 NOI 竞赛中,这类题目是构建搜索剪枝、状态压缩动态规划以及组合数学的基础。


一、 题目描述 (NOI 风格)

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

【问题描述】 给定一个包含 nn 个不重复正整数的集合 SS。请设计一个算法,输出该集合的所有可能子集(幂集)。 注意:输出的子集中,元素的顺序不作要求,子集之间的顺序也不作要求。

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

【输出格式】 输出若干行,每行代表一个子集。元素之间用空格隔开。空集请保留一个空行或以特殊标记处理(在本题中,直接输出所有组合即可)。

由于子集的遍历顺序可能不一样,可能有多种结果。本题测评使用了SPJ程序测评,只要求任意一种满足要求的结果就行

【样例输入】

3
1 2 3

【样例输出】

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

【数据范围】 对于 100%100\% 的数据:1n161 \le n \le 16,集合中的元素互不相同,且每个元素均在 int 范围内。


二、 预备知识点

  1. 递归与回溯 (Recursion & Backtracking):理解搜索树的生成过程。
  2. 位运算 (Bit Manipulation):利用二进制状态压缩表示集合。
  3. 组合数学:长度为 nn 的集合,其子集总数为 2n2^n
  4. STL 容器std::vector 的基本操作。

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

现在,请拿出你的草稿纸,我们不直接写代码,而是画出逻辑。

1. 决策树法(启发式思考)

假设集合是 {1, 2, 3}。对于每一个元素,你在构建子集时只有两种选择:不选

  • 第 1 步:考察元素 1
    • 分支 A:放入子集。
    • 分支 B:不放入子集。
  • 第 2 步:在上述两个分支的基础上,考察元素 2
    • 分支 A 派生出:{1, 2}{1}
    • 分支 B 派生出:{2}{}
  • 第 3 步:以此类推考察元素 3

思考:你会发现这构成了一棵深度为 nn满二叉树。叶子节点的总数正好是 2n2^n

2. 状态压缩法(图形化思考)

如果你把“选”记作 1,“不选”记作 0

  • 子集 {1, 3} 对应二进制 101 (十进制 5)。
  • 子集 {2} 对应二进制 010 (十进制 2)。
  • 所有子集正好对应整数范围 [0,2n1][0, 2^n - 1] 里的每一个数字。

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

  • 时间复杂度分析

    • 总共有 2n2^n 个状态。
    • 每个状态需要 O(n)O(n) 的时间构造或输出。
    • 总复杂度:O(n2n)O(n \cdot 2^n)
    • 优化建议:在 nn 较小时,递归和位运算效率差异不大;当 n>20n > 20 时,单纯的子集枚举将超时,需考虑剪枝或动态规划。
  • 空间复杂度分析

    • 递归深度为 O(n)O(n)
    • 如果需要存储所有结果,空间为 O(n2n)O(n \cdot 2^n)。在 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 搜索类题目时,看到以下关键词要形成条件反射:

  1. “所有可能的” (All possible):暗示这可能是一道搜索题(DFS/BFS)或者动态规划题,需要遍历整个解空间。
  2. “互不相同/不重复” (Distinct/Unique):这是好消息,说明你不需要处理“去重”逻辑(如 if (i > start && nums[i] == nums[i-1]) continue)。
  3. “子集” (Subsets/Power Set):立刻想到解的数量是 2n2^n。检查 nn 的范围,如果 n20n \le 20,可以直接暴力搜索;如果 nn 很大,必然有贪心、剪枝或数学规律。
  4. “不限顺序” (In any order):意味着你不需要在搜索过程中进行复杂的排序处理来保证输出顺序。

教练寄语: 子集问题是搜索的灵魂。理解了“选与不选”的决策树,你就开启了通往深度优先搜索(DFS)的大门。尝试用位运算和递归两种方式都实现一遍,这会对你理解计算机底层逻辑非常有帮助!加油!