LeetCode 90. 子集 II

1 阅读4分钟

LeetCode 90. 子集 II

摘要

LeetCode 90 子集 II 题解,采用回溯法在决策树上深度优先遍历,通过排序和横向剪枝(跳过同一层重复元素)避免重复子集,时间复杂度 O(n × 2^n),空间复杂度 O(n × 2^n)(结果存储)。

标签

#算法 #LeetCode #回溯 #DFS #数组 #剪枝 #题解

目录

  1. 题目描述
  2. 易错点
  3. 思路
  4. 编写代码

1. 题目描述

给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

text

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

示例 2:

text

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

2. 易错点

  1. 必须排序:只有排序后,重复元素才会相邻,才能在遍历时通过 nums[i] == nums[i-1] 判断是否重复。
  2. 去重剪枝发生在横向(同一层),而非纵向(不同层)if (i > start && nums[i] == nums[i-1]) continue; 这一行只跳过当前 for 循环中与前一元素相同的选项(即同一决策节点下的不同树枝),但允许不同深度(纵向)出现相同值,因为那是不同位置的选择。
  3. 每层递归都要记录路径:与子集 I 相同,所有中间状态都是有效子集。
  4. 路径拷贝:每次记录时需将 path 拷贝到新内存,存入 ans
  5. 结果数组容量:子集总数最多为 2^n,分配 1 << numsSize 足够。

3. 思路

为什么要排序?

在包含重复元素的数组中,如果不对数组排序,重复元素可能分散在不同位置,无法通过简单的相邻比较来检测重复组合。排序后,所有相同的元素会连续排列,这样在回溯的每一层中,只需检查当前元素是否与前一个元素相同,就能决定是否跳过该分支,从而避免生成重复的子集。

例如:nums = [2,2],若不排序,子集生成过程仍可能产生重复,但排序后重复元素相邻,便于剪枝。

决策树模型

本题的决策树与子集 I 类似,但新增了横向剪枝策略。每个节点代表已选择的元素集合,边代表选择某个元素。为了保证子集不重复,需要在树的同一层(即同一个 for 循环中)避免选择相同的值,因为不同位置选择相同的值会导致相同的子集(只要排序后值相同,顺序固定,组合就一样)。

横向剪枝 vs 纵向允许

  • 横向(同一层) :在同一递归层级中,for 循环遍历 start 到 numsSize-1。如果当前元素与前一个元素相同(nums[i] == nums[i-1]),则跳过。这是因为在同一决策节点下,选择第一个 2 和选择第二个 2 生成的后继子集是完全一样的(例如 [1,2] 和 [1,2] 重复),因此只需保留第一个。
  • 纵向(不同层) :不同递归深度允许出现相同的值。例如 [1,2,2] 中,第二层选了第一个 2,第三层还可以选第二个 2,生成 [1,2,2],这是合法的。纵向的重复不受剪枝影响,因为此时 i 不等于 starti 是当前位置的索引,start 是当前层起始索引),条件 i > start 为假,所以不会跳过。

回溯过程

  1. 对 nums 进行升序排序。

  2. 定义回溯函数 backtracking(start)

    • 将当前 path(长度为 pathTop)拷贝到 ans,记录长度。

    • 遍历 i = start 到 numsSize-1

      • 若 i > start && nums[i] == nums[i-1],跳过(横向去重)。
      • 否则,将 nums[i] 加入 path,递归 backtracking(i+1),然后 pathTop-- 回溯。
  3. 主函数初始化全局变量,调用回溯,返回结果。

剪枝条件详解

if (i > start && nums[i] == nums[i-1]) continue; 中的 i > start 确保只在同一层比较相邻元素。start 是当前层的起始索引,当 i == start 时,即使 nums[i] == nums[i-1](但 i-1 不在本层范围内,实际上 i-1 可能是上一层的元素),此时不应跳过,因为这是该层第一次遇到该值,必须保留。

例如 nums = [1,2,2]start = 0 时,i=0 选 1,递归;i=1 选第一个 2,递归;i=2 时 i > start 且 nums[2]==nums[1],跳过,避免生成重复子集 [1,2](已由 i=1 生成)。


4. 编写代码

c 运行

c

#include <stdlib.h>

int** ans;
int* path;
int ansTop;
int pathTop;

int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

void backtracking(int* nums, int numsSize, int start, int** returnColumnSizes) {
    int* temp = (int*)malloc(sizeof(int) * pathTop);
    for (int i = 0; i < pathTop; i++) temp[i] = path[i];
    ans[ansTop] = temp;
    (*returnColumnSizes)[ansTop] = pathTop;
    ansTop++;

    for (int i = start; i < numsSize; i++) {
        if (i > start && nums[i] == nums[i-1]) continue;
        path[pathTop++] = nums[i];
        backtracking(nums, numsSize, i + 1, returnColumnSizes);
        pathTop--;
    }
}

int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    qsort(nums, numsSize, sizeof(int), cmp);
    int maxSubsets = 1 << numsSize;
    ans = (int**)malloc(sizeof(int*) * maxSubsets);
    *returnColumnSizes = (int*)malloc(sizeof(int) * maxSubsets);
    path = (int*)malloc(sizeof(int) * numsSize);
    ansTop = 0;
    pathTop = 0;

    backtracking(nums, numsSize, 0, returnColumnSizes);

    *returnSize = ansTop;
    return ans;
}