LeetCode 78:子集(两种解法详解:枚举扩展 & DFS 回溯)

1 阅读2分钟

一、题目要求

给定一个不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

解集不能包含重复的子集,子集中的元素顺序可以任意。

示例:

输入:nums = [1,2,3]
输出:
[
  [],
  [1],
  [2],
  [3],
  [1,2],
  [1,3],
  [2,3],
  [1,2,3]
]

二、核心理解:什么是“子集问题”

子集问题的本质是:

对于数组中的每一个元素
都存在两种选择:选 or 不选

如果数组长度是 n,那么最终子集的数量一定是:

2^n

本题可以用两种完全不同但都非常经典的方式解决:

  1. 迭代枚举(在已有子集的基础上扩展)
  2. DFS + 回溯(把问题看成一棵决策树)

下面分别展开。


三、方法一:枚举扩展法(迭代构造子集)

思路说明

从空集开始:

res = [[]]

每遍历到一个新数字 x,就做一件事:

  • 把当前 res 中的每一个子集
  • 复制一份
  • 在复制的子集后面加上 x
  • 再统一加入 res

一句话概括:

新元素 = 让所有已有子集“多一条分支”


代码实现

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        // 初始只有一个空集
        res.add(new ArrayList<>());

        for (int num : nums) {
            List<List<Integer>> subset = new ArrayList<>();

            // 遍历当前已有的所有子集
            for (List<Integer> list : res) {
                List<Integer> temp = new ArrayList<>(list);
                temp.add(num);
                subset.add(temp);
            }

            // 把新生成的子集加入结果集中
            for (List<Integer> l : subset) {
                res.add(l);
            }
        }
        return res;
    }
}

过程示例(nums = [1,2,3])

初始:

res = [[]]

处理 1:

新增:[1]
res = [], [1]

处理 2:

已有:[], [1]
新增:[2], [1,2]
res = [], [1], [2], [1,2]

处理 3:

新增:[3], [1,3], [2,3], [1,2,3]

特点总结

  • 不需要递归
  • 思路直观,非常适合理解子集的“扩展本质”
  • 本质是一个“滚雪球”的过程

四、方法二:DFS + 回溯(树形搜索)

思路说明

把问题想象成一棵树:

  • 每一层,决定是否选某个元素
  • 每一个节点,都是一个合法子集
  • 走到哪里,都可以把当前路径加入结果

核心思想是:

子集问题 = 从任意起点开始的组合问题
不强调“结束条件”,而是“每一步都是答案”


代码实现

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        dfs(res, nums, 0, new ArrayList<>());
        return res;
    }

    private void dfs(List<List<Integer>> res, int[] nums, int index, List<Integer> subset) {
        // 每一个状态都是一个合法子集
        res.add(new ArrayList<>(subset));

        if (index == nums.length) {
            return;
        }

        for (int i = index; i < nums.length; i++) {
            subset.add(nums[i]);
            dfs(res, nums, i + 1, subset);
            subset.remove(subset.size() - 1); // 回溯
        }
    }
}

DFS 过程理解

[1,2,3] 为例:

[]
 ├─ [1]
 │   ├─ [1,2]
 │   │   └─ [1,2,3]
 │   └─ [1,3]
 ├─ [2]
 │   └─ [2,3]
 └─ [3]

关键点:

  • index 保证不走回头路
  • subset 代表当前路径
  • 回溯是为了“撤销选择,走另一条分支”

为什么要 new ArrayList(subset)

因为 subset 在 DFS 过程中会不断被修改:

  • add
  • remove

如果直接把 subset 放进 res,最终所有结果都会指向同一个引用,内容会被覆盖。