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

LeetCode 39.组合总和:回溯法与剪枝优化的完美结合

一、问题本质与形式化定义

1.1 题目形式化描述

  • 输入:无重复整数数组candidates、目标值target
  • 输出:所有和为target的组合集合,满足:
    • 元素可重复使用
    • 组合内元素非降序(避免重复解)
    • 解集无重复组合

1.2 问题本质分析

属于无界组合问题(元素可重复选),与0-1组合问题的核心区别在于:

  • 0-1组合:每个元素只能选0或1次(对应背包问题中的0-1背包)
  • 无界组合:每个元素可选0到多次(对应完全背包问题)

二、回溯法核心实现与状态机模型

2.1 回溯算法的标准框架

void backtracking(参数) {if (终止条件) {处理结果;return;}for (选择:本层可选择的所有选项) {做出选择;backtracking(下一层参数);撤销选择;}
}

本问题中对应的关键映射:

  • 选择:将candidates[i]加入当前组合
  • 终止条件:sum == targetsum > target
  • 下一层参数:start=i(允许重复选当前元素)

2.2 状态空间树可视化

candidates=[2,3,6,7], target=7为例,状态树结构:

          根(0)/   |   \2     3     6(剪枝)    7/ | \   |2  3  6(剪) 3/ |     |
2  3(7) 6(剪)
  • 树的深度:最大为target/最小元素(本例中为7/2=3层)
  • 分支数:每层最多为candidates.length - start

三、代码逐行解析与关键技术点

3.1 初始化与排序处理

Arrays.sort(candidates); // 核心优化点

排序的三大作用:

  1. 剪枝基础:确保后续元素递增,可通过sum + candidates[i] > target提前终止
  2. 去重保证:固定组合内元素顺序(如[2,3]不会出现[3,2])
  3. 逻辑简化:使递归过程中的选择具有有序性

3.2 回溯函数深度解析

void backtracking(int[] candidates, int target, int start, int sum) {// 终止条件1:找到合法组合if (sum == target) {res.add(new ArrayList<>(temp)); // 注意深拷贝return;}// 终止条件2:超过目标和(剪枝点1)if (sum > target) {return;}// 本层选择遍历(从start开始)for (int i = start; i < candidates.length; i++) {// 剪枝点2:当前选择必超目标和,后续更大元素直接跳过if (sum + candidates[i] > target) break;// 状态变更:选择当前元素sum += candidates[i];temp.add(candidates[i]);// 递归:允许重复选当前元素(i不变)backtracking(candidates, target, i, sum);// 状态回溯:撤销选择sum -= candidates[i];temp.removeLast();}
}
3.2.1 深拷贝的必要性
  • 为什么不能直接res.add(temp)
    • temp是引用类型,后续回溯会修改其内容
    • 示例:当找到[2,2,3]后,temp会被回溯清空,若不拷贝则结果集存储的是空列表
3.2.2 start参数的核心作用
  • 控制组合唯一性的关键:
    • start=i时,同一层不会重复选前面的元素
    • 例如:第一层选2后,第二层只能从i=0(即2)开始选,保证组合内元素非降序

四、剪枝策略的数学证明与效率分析

4.1 剪枝点的数学推导

定理:若数组已排序,当sum + candidates[i] > target时,后续元素candidates[j] (j>i)必然满足sum + candidates[j] > target

  • 证明:
    • 因数组排序,故candidates[j] >= candidates[i]
    • sum + candidates[j] >= sum + candidates[i] > target
  • 推论:此时可直接break退出循环,避免无效递归

4.2 时间复杂度精确分析

  • 最坏情况(所有元素为1,target=n):
    • 组合数为C(n-1,0)+C(n-1,1)+…+C(n-1,n-1)=2^(n-1)
    • 每层递归时间复杂度为O(1)(不考虑深拷贝)
    • 总时间复杂度:O(2^target)
  • 优化后时间复杂度:
    • 设数组最小元素为m,则递归深度最大为target/m
    • 实际复杂度:O(k * 2^(target/m)),k为平均分支因子

五、边界情况与扩展问题

5.1 特殊输入处理

  • candidates为空或所有元素都大于target时:
    • 直接返回空列表,无需递归
  • target=0时:
    • 需特殊处理空组合是否合法(本题中target≥1)

5.2 扩展问题:含重复元素的组合总和

candidates含重复元素(如[1,1,2]),需增加去重逻辑:

for (int i = start; i < candidates.length; i++) {// 同一层中跳过相同元素(去重核心)if (i > start && candidates[i] == candidates[i-1]) continue;// 后续逻辑不变
}

原理:保证同一层中相同元素只处理一次,避免[1,2][1,2]这样的重复组合

六、算法执行过程动态演示

6.1 示例输入完整流程

candidates=[2,3,6,7], target=7为例,递归调用栈变化:

  1. 初始调用:backtracking([2,3,6,7],7,0,0)
  2. i=0(元素2):
    • sum=0+2=2 ≤7,加入temp→[2]
    • 递归调用:backtracking([...],7,0,2)
  3. 第二层i=0:
    • sum=2+2=4 ≤7,加入temp→[2,2]
    • 递归调用:backtracking([...],7,0,4)
  4. 第三层i=0:
    • sum=4+2=6 ≤7,加入temp→[2,2,2]
    • 递归调用:backtracking([...],7,0,6)
  5. 第四层i=0:
    • sum=6+2=8 >7,回溯→sum=6,temp→[2,2,2]
    • i=1(元素3):sum=6+3=9>7,回溯
    • i=2(元素6):sum=6+6=12>7,回溯
    • i=3(元素7):sum=6+7=13>7,回溯
    • 回到第三层,sum=4,temp→[2,2]
  6. 第三层i=1(元素3):
    • sum=4+3=7=target,记录组合[2,2,3]
    • 回溯→sum=4,temp→[2,2]
    • 后续i=2、3均超7,回到第二层
  7. 第二层i=1(元素3):
    • sum=2+3=5 ≤7,加入temp→[2,3]
    • 递归调用:backtracking([...],7,1,5)
    • 后续选择3时sum=5+3=8>7,回溯
    • 选择6、7均超,回到第一层
  8. 第一层i=1(元素3):
    • sum=0+3=3 ≤7,加入temp→[3]
    • 递归调用中选择3+3+3=9>7,最终无合法组合
  9. 第一层i=3(元素7):
    • sum=0+7=7=target,记录组合[7]

七、同类问题拓展与解题模板

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;}if (target < 0) return;for (int i = start; i < nums.length; i++) {// 剪枝条件(根据题目调整)if (nums[i] > target) break;path.add(nums[i]);backtrack(nums, target - nums[i], i); // 可重复选:i不变// backtrack(nums, target - nums[i], i+1); // 不可重复选:i+1path.remove(path.size() - 1);}
}

7.2 相关LeetCode题目拓展

  • 39.组合总和(本题,元素可重复)
  • 40.组合总和II(元素不可重复,含重复元素)
  • 216.组合总和III(选k个不同数字)
  • 377.组合总和IV(组合顺序不同算不同解)

八、常见误区与调试技巧

8.1 典型错误分析

  1. 重复组合问题

    • 错误原因:未控制start参数,导致[2,3][3,2]同时出现
    • 解决方案:确保递归时传递i而非i+1,并通过排序固定顺序
  2. 深拷贝遗漏

    • 错误现象:结果集所有组合最终都为空
    • 原因:直接存储temp引用,后续回溯修改了其内容

8.2 调试技巧

  1. 打印递归轨迹
    System.out.println("当前组合:" + temp + ", 和:" + sum + ", 起始位置:" + start);
    
  2. 可视化状态树
    • 使用递归深度和当前选择绘制树状图
    • 标记剪枝发生的节点位置

九、总结:回溯法解决组合问题的核心要素

  1. 状态定义

    • path记录当前组合,sum记录当前和
    • start参数控制组合唯一性
  2. 递归设计

    • 终止条件:和为target或超过target
    • 选择逻辑:当前层可选择的元素范围
  3. 优化策略

    • 排序+剪枝减少搜索空间
    • 深拷贝避免结果污染

掌握这三个核心要素,即可高效解决各类组合搜索问题,从基础的组合总和到复杂的排列组合问题均能迎刃而解。

http://www.xdnf.cn/news/10366.html

相关文章:

  • CCPC dongbei 2025 F
  • 组件化:软件工程化的基础
  • 接口安全SOAPOpenAPIRESTful分类特征导入项目联动检测
  • 树莓派3B小练习
  • IT技术文章汇总
  • 美业+智能体,解锁行业转化新密码(2/6)
  • 大白话 Seata 分布式事务浅析,详解TCC模式
  • 腾讯位置商业授权行政区划开发指南
  • Codeforces Round 1028 (Div. 2) B. Gellyfish and Baby‘s Breath
  • Nginx反向代理
  • NodeJS全栈开发面试题讲解——P12高性能场景题
  • Chorme如何对于youtube视频进行画中画背景播放?
  • 多模态AI的企业应用场景:视觉+语言模型的商业价值挖掘
  • 8天Python从入门到精通【itheima】-62~63
  • 结合源码分析Redis的内存回收和内存淘汰机制,LRU和LFU是如何进行计算的?
  • 深度学习|pytorch基本运算-乘除法和幂运算
  • 初识PS(Photoshop)
  • 【Oracle】安装单实例
  • 【Go】2、Go语言实战
  • python打卡day42@浙大疏锦行
  • 动态库导出符号与extern “C“
  • 2025年05月总结及随笔之家电询价及以旧换新
  • 剪枝中的 `break` 与 `return` 区别详解
  • APM32主控键盘全功能开发实战教程:软件部分
  • 【论文解读】Deformable DETR | Deformable Transformers for End-to-End Object Detection
  • 题单:最大公约数(辗转相除法)
  • 安全漏洞修复导致SpringBoot2.7与Springfox不兼容
  • 爬虫工具链的详细分类解析
  • 力扣刷题Day 66:分割回文串(131)
  • 【Redis】数据类型补充