LeetCode 90. 子集 II
摘要
LeetCode 90 子集 II 题解,采用回溯法在决策树上深度优先遍历,通过排序和横向剪枝(跳过同一层重复元素)避免重复子集,时间复杂度 O(n × 2^n),空间复杂度 O(n × 2^n)(结果存储)。
标签
#算法 #LeetCode #回溯 #DFS #数组 #剪枝 #题解
目录
- 题目描述
- 易错点
- 思路
- 编写代码
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. 易错点
- 必须排序:只有排序后,重复元素才会相邻,才能在遍历时通过
nums[i] == nums[i-1]判断是否重复。 - 去重剪枝发生在横向(同一层),而非纵向(不同层) :
if (i > start && nums[i] == nums[i-1]) continue;这一行只跳过当前for循环中与前一元素相同的选项(即同一决策节点下的不同树枝),但允许不同深度(纵向)出现相同值,因为那是不同位置的选择。 - 每层递归都要记录路径:与子集 I 相同,所有中间状态都是有效子集。
- 路径拷贝:每次记录时需将
path拷贝到新内存,存入ans。 - 结果数组容量:子集总数最多为
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不等于start(i是当前位置的索引,start是当前层起始索引),条件i > start为假,所以不会跳过。
回溯过程
-
对
nums进行升序排序。 -
定义回溯函数
backtracking(start):-
将当前
path(长度为pathTop)拷贝到ans,记录长度。 -
遍历
i = start到numsSize-1:- 若
i > start && nums[i] == nums[i-1],跳过(横向去重)。 - 否则,将
nums[i]加入path,递归backtracking(i+1),然后pathTop--回溯。
- 若
-
-
主函数初始化全局变量,调用回溯,返回结果。
剪枝条件详解
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;
}