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

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. 优化策略

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

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

相关文章:

  • Go语言常见接口设计技巧-《Go语言实战指南》
  • 题单:最大公约数(辗转相除法)
  • 数据结构之ArrayList
  • 【C/C++】面试基础题目收集
  • 基于 Three.js 的文本粒子解体效果技术原理剖析
  • 解释程序(Python)不需要生成机器码 逐行解析 逐行执行
  • java27
  • Maven概述,搭建,使用
  • 第五篇:HTTPS 与 TLS/SSL 握手原理
  • 前端面经 两栏布局
  • 逆向入门(1)
  • DFS每日刷题
  • Mac电脑上本地安装 redis并配置开启自启完整流程
  • pikachu通关教程-CSRF
  • 使用langchain实现五种分块策略:语义分块、父文档分块、递归分块、特殊格式、固定长度分块
  • 【论文阅读 | PR 2024 |ICAFusion:迭代交叉注意力引导的多光谱目标检测特征融合】
  • 一天搞懂深度学习--李宏毅教程笔记
  • 财管8-企业价值评估
  • 【位运算】两整数之和(medium)
  • 过滤攻击-聚合数据
  • 临泉网站建设/数据统计网站
  • 广西钦州有人帮做网站的公司吗/快速排名优化推广价格
  • 英文网站模板源代码/产品经理培训
  • 电子商务网站系统规划报告/企业产品推广策划方案
  • 微信手机官方网站/网站排名费用
  • 西安千锋教育培训机构怎么样/seo站长综合查询工具