当前位置: 首页 > news >正文

LeetCode 40.组合总和II:含重复元素的组合问题去重策略详解

一、问题本质与核心差异

1.1 题目要求

给定一个可能含重复元素的整数数组candidates和目标值target,找出所有和为target的组合,要求:

  • 每个元素在每个组合中只能使用一次
  • 解集不能包含重复的组合

1.2 与组合总和I的关键区别

对比项组合总和I(39题)组合总和II(40题)
元素使用可重复使用每个元素只能用一次
输入特性无重复元素可能含重复元素
去重需求无需去重(排序保证)必须显式去重
递归参数backtracking(i)backtracking(i+1)

二、去重核心逻辑:i > start的数学原理

2.1 代码中的去重关键段

for (int i = start; i < candidates.length; i++) {// 核心去重条件if (i > start && candidates[i] == candidates[i - 1]) {continue;}// 后续选择逻辑
}

2.2 去重条件的严格证明

定理:当数组排序后,若i > startcandidates[i] == candidates[i-1],则选择candidates[i]必然产生与选择candidates[i-1]相同的组合

  • 证明
    1. 因数组已排序,故candidates[i] == candidates[i-1]
    2. i > start表示i-1在当前层已被处理过(start为当前层起始索引)
    3. 若选择i,其后续递归路径与选择i-1的路径完全对称(因元素值相同)
    4. 因此会生成重复组合,需跳过

2.3 去重场景可视化

candidates=[1,1,2], target=2为例:

排序后:[1,1,2]
第一层(start=0):
i=0(1): 选择→进入第二层(start=1)
i=1(1): i>start(1>0)且值相同→跳过
i=2(2): sum+2=2→记录[1,2]第二层(start=1):
i=1(1): 选择→进入第三层(start=2)
i=2(2): sum+2=2→记录[1,2](与上层重复?不,因start不同)

关键:去重条件仅跳过同一层中的重复元素,不同层的相同元素可正常选择

三、代码深度解析与状态管理

3.1 整体框架对比

// 组合总和I(可重复选)
backtracking(candidates, target, i, sum); // 递归时i不变// 组合总和II(不可重复选)
backtracking(candidates, target, i+1, sum); // 递归时i+1

3.2 状态转移图

                          根(0,sum=0)/   |   \1     1     2/ | \   |1  2  ... 2/2 (sum=2)
  • 同一层中第二个1被跳过(i=1>start=0且值相同)
  • 不同层的1(如第一层选1后,第二层选1是允许的)

3.3 深拷贝与状态回溯

if (sum == target) {res.add(new ArrayList<>(temp)); // 必须深拷贝
}
// 回溯时撤销选择
sum -= candidates[i];
temp.removeLast();

注意:若不使用深拷贝,当回溯修改temp时,结果集中的引用会被同步修改

四、剪枝策略与复杂度分析

4.1 双重剪枝优化

  1. 值剪枝sum + candidates[i] > target时break(同39题)
  2. 重复剪枝i > start && candidates[i] == candidates[i-1]时continue

4.2 时间复杂度精确计算

  • 最坏情况(无重复元素且每个元素≤target):
    • 组合数为C(n,0)+C(n,1)+…+C(n,k)≈2^n
    • 总时间复杂度:O(2^n × k),k为平均组合长度
  • 优化后复杂度:
    • 重复元素越多,剪枝效果越明显
    • 实际复杂度:O(2^m × k),m为去重后的唯一元素个数

4.3 空间复杂度

  • 递归栈深度:O(target/min(candidates))
  • 临时空间:O(target)(存储当前组合)
  • 总空间复杂度:O(target + 2^m)

五、典型案例执行流程

5.1 案例输入

candidates=[10,1,2,7,6,1,5], target=8
排序后:[1,1,2,5,6,7,10]

5.2 递归执行轨迹

  1. 第一层(start=0):

    • i=0(1): sum=1≤8→选择,进入start=1
    • i=1(1): i>0且值相同→跳过
    • i=2(2): sum=2≤8→选择,进入start=3
    • …(中间过程省略)
    • i=5(7): sum=7≤8→选择,进入start=6
    • i=6(10): sum+10=17>8→break
  2. 第二层(start=1)处理i=2(2)时:

    • sum=1+2=3≤8→选择,进入start=3
    • i=3(5): sum+5=8→记录组合[1,2,5]
  3. 去重演示:

    • 第一层i=0和i=1均为1,但i=1时i>start(0)→跳过
    • 确保不会生成[1,1,…]和[1,1,…]的重复组合

5.3 最终解集

[[1,1,6], [1,2,5], [1,7], [2,6]]

六、去重逻辑的常见误区

6.1 错误去重条件对比

错误条件问题表现正确条件
i > 0 && candidates[i]==candidates[i-1]跳过所有重复元素,包括不同层的合法选择i > start && ...
不排序直接去重无法保证重复元素相邻,去重失效必须先排序

6.2 为什么必须先排序?

  • 排序后重复元素相邻,才能通过candidates[i]==candidates[i-1]检测重复
  • 示例:未排序数组[1,2,1],去重条件无法检测到第一个和第三个1

七、扩展问题与解题模板

7.1 通用组合问题模板(含去重)

List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
void backtrack(int[] nums, int target, int start) {if (target == 0) {res.add(new ArrayList<>(path));return;}for (int i = start; i < nums.length; i++) {// 剪枝1:值超过目标if (nums[i] > target) break;// 剪枝2:去重(核心)if (i > start && nums[i] == nums[i-1]) continue;path.add(nums[i]);backtrack(nums, target - nums[i], i + 1); // 不可重复选path.remove(path.size() - 1);}
}

7.2 变种问题处理

  1. 元素可重复且含重复元素

    • 递归参数改为backtrack(i)(同39题)
    • 去重条件不变,但允许不同层选相同元素
  2. 组合顺序不同算不同解

    • 去掉start参数,每次从0开始搜索
    • 如LeetCode 377.组合总和IV

八、调试与优化技巧

8.1 可视化去重过程

// 调试打印
System.out.println("当前层start=" + start + ", i=" + i + ", num=" + candidates[i] + ", 去重条件:" + (i > start && candidates[i] == candidates[i-1]));

8.2 大数据集优化

  1. 提前去重
    // 预处理去重+排序
    Arrays.sort(candidates);
    List<Integer> unique = new ArrayList<>();
    for (int i=0; i<candidates.length; i++) {if (i==0 || candidates[i]!=candidates[i-1]) {unique.add(candidates[i]);}
    }
    
  2. 记忆化搜索:对相同target的子问题缓存结果

九、总结:含重复元素的组合问题解题框架

  1. 三步核心流程

    • 排序数组使重复元素相邻
    • 回溯时用start控制元素使用范围
    • 通过i > start && nums[i]==nums[i-1]跳过重复选择
  2. 关键参数理解

    • start:当前层可选择的最小索引,避免重复组合
    • i > start:确保同一层中相同元素只处理一次
    • i+1:递归时跳过当前元素,实现"每个元素只用一次"
  3. 拓展应用

    • 子集问题(78题)去重
    • 排列问题(46题)去重
    • 组合总和III(216题)等变体

掌握这套去重策略,可系统性解决所有含重复元素的组合搜索问题,核心在于理解"层内去重"与"层间允许"的逻辑差异。

相关文章:

  • 动态库导出符号与extern “C“
  • Python训练营打卡 Day42
  • CppCon 2014 学习:ASYNC SEQUENCES AND ALGORITHMS
  • golang -- slice 底层逻辑
  • javaEE->多线程:定时器
  • 【Java学习笔记】枚举
  • 初学大模型部署以及案例应用(windows+wsl+dify+mysql+Ollama+Xinference)
  • python打卡day42
  • Mask_RCNN 环境配置及训练
  • leetcode hot100 二叉树(一)
  • 第七部分:第四节 - 在 NestJS 应用中集成 MySQL (使用 TypeORM):结构化厨房的原材料管理系统
  • 剑指offer hot100 第三周
  • 查看make命令执行后涉及的预编译宏定义的值
  • java synchronized关键字用法
  • io流2——字节输入流,文件拷贝
  • Codeforces 1027 Div3(ABCDEF)
  • Java网络编程基础:从阻塞式I/O到线程池模型
  • DAY 34 超大力王爱学Python
  • C++ —— STL容器——string类
  • ps中通过拷贝的图层和通过剪切的图层
  • 申请电子邮箱免费注册/成都高薪seo
  • 网站建设的公司第七页/武汉百度开户电话
  • 网站免费推广怎么做/商丘优化公司
  • 答题卡在线制作网站/google商店
  • 网站更新内容怎么做/手游推广渠道和推广方式
  • 猪八戒网站做推广靠谱/关键词收录