2026/4/18 8:30:52
网站建设
项目流程
要制作自己的网站需要什么材料,网站建设域名注册熊掌号,新乡网站建设方案,ui设计培训机构有用吗Java版LeetCode热题100之组合总和#xff1a;回溯算法与剪枝优化的深度解析 摘要#xff1a;本文将深入剖析 LeetCode 热题 100 中的经典回溯问题——组合总和#xff08;Combination Sum#xff09;。我们将从题目出发#xff0c;系统讲解回溯算法的核心思想、递归结构设…Java版LeetCode热题100之组合总和回溯算法与剪枝优化的深度解析摘要本文将深入剖析 LeetCode 热题 100 中的经典回溯问题——组合总和Combination Sum。我们将从题目出发系统讲解回溯算法的核心思想、递归结构设计并重点探讨剪枝优化策略。文章提供完整可运行的 Java 实现涵盖时间/空间复杂度分析、常见面试问题、实际应用场景、相关题目推荐等模块帮助读者不仅掌握解题技巧更能理解其背后的算法思想与工程价值。一、原题回顾题目名称组合总和题目编号LeetCode 39难度等级中等题目描述给你一个无重复元素的整数数组candidates和一个目标整数target找出candidates中可以使数字和为目标数target的所有不同组合并以列表形式返回。你可以按任意顺序返回这些组合。同一个数字可以无限制重复被选取如果至少一个数字的被选数量不同则两种组合是不同的保证和为 target 的不同组合数少于 150 个示例示例 1 输入candidates [2,3,6,7], target 7 输出[[2,2,3],[7]] 解释2 2 3 77 7 示例 2 输入: candidates [2,3,5], target 8 输出: [[2,2,2,2],[2,3,3],[3,5]] 示例 3 输入: candidates [2], target 1 输出: []提示1 candidates.length 302 candidates[i] 40candidates的所有元素互不相同1 target 40二、原题分析本题是一个经典的完全背包问题的变种但要求输出所有可行解而非最优解数量。关键特征元素可重复使用→ 完全背包特性无重复元素→ 无需去重处理组合而非排列→[2,2,3]和[2,3,2]视为同一组合目标明确→ 和恰好等于target由于target ≤ 40且candidates[i] ≥ 2最大递归深度为40/2 20属于可接受范围适合使用回溯法Backtracking。关键洞察为了避免生成重复组合如[2,3,2]我们需要按顺序选择元素即一旦跳过某个元素后续就不能再选择它。三、答案构思3.1 回溯法的核心思想对每个元素我们有两个选择跳过当前元素→ 处理下一个元素选择当前元素→ 仍可继续选择当前元素因可重复通过维护一个起始索引start index确保组合按非递减顺序生成从而避免重复。3.2 算法步骤排序可选但推荐便于剪枝优化回溯函数结束条件1target 0→ 找到有效组合结束条件2target 0或idx candidates.length→ 无效路径选择1跳过当前元素递归idx 1选择2选择当前元素若target candidates[idx]递归idx保持索引不变3.3 剪枝优化的重要性排序后剪枝若candidates[idx] target则后续所有元素都更大可直接终止提前终止减少无效递归调用四、完整答案Java 实现4.1 基础版官方题解classSolution{publicListListIntegercombinationSum(int[]candidates,inttarget){ListListIntegerresultnewArrayList();ListIntegerpathnewArrayList();backtrack(candidates,target,0,path,result);returnresult;}privatevoidbacktrack(int[]candidates,inttarget,intstart,ListIntegerpath,ListListIntegerresult){// 找到有效组合if(target0){result.add(newArrayList(path));return;}// 遍历从 start 开始的元素for(intistart;icandidates.length;i){// 剪枝当前元素已超过剩余目标if(candidates[i]target){break;}// 做选择path.add(candidates[i]);// 递归仍从 i 开始允许重复backtrack(candidates,target-candidates[i],i,path,result);// 撤销选择path.remove(path.size()-1);}}}4.2 优化版预排序 强剪枝classSolution{publicListListIntegercombinationSum(int[]candidates,inttarget){// 排序便于剪枝Arrays.sort(candidates);ListListIntegerresultnewArrayList();backtrack(candidates,target,0,newArrayList(),result);returnresult;}privatevoidbacktrack(int[]candidates,inttarget,intstart,ListIntegerpath,ListListIntegerresult){if(target0){result.add(newArrayList(path));return;}for(intistart;icandidates.length;i){// 强剪枝当前元素过大后续更不可能if(candidates[i]target){break;}path.add(candidates[i]);backtrack(candidates,target-candidates[i],i,path,result);path.remove(path.size()-1);}}}✅关键改进预排序使剪枝更有效for 循环替代双递归代码更简洁逻辑更清晰统一剪枝条件在循环内判断五、代码分析5.1 为什么用start参数防止重复组合确保组合按非递减顺序生成示例candidates [2,3,6,7]先选 2后续可选 2,3,6,7跳过 2 后选 3后续只能选 3,6,7不能再选 2效果天然避免[2,3,2]这类重复5.2 递归树结构以candidates [2,3,6,7],target 7为例[] / | | \ [2][3][6][7] /| | | [2,2][2,3] [7]✓ / | | [2,2,2][2,2,3]✓ | [2,2,2,2]✗ (target-1)✓有效组合✗无效路径target 0每层只考虑从当前索引开始的元素5.3 剪枝机制详解无排序的剪枝if(candidates[i]target)continue;只跳过当前元素但后续可能有更小元素因未排序有排序的强剪枝Arrays.sort(candidates);// ...if(candidates[i]target)break;因数组已排序candidates[i] target意味着所有后续元素都 target可直接break终止整个循环性能对比对于candidates [7,6,3,2],target 7无排序需检查所有元素有排序[2,3,6,7]当i3时77?不成立但若target5则65时可break5.4 深拷贝的重要性result.add(newArrayList(path));必须创建副本否则所有组合将指向同一个path对象最终结果会全部等于最后一次path的状态六、时间复杂度与空间复杂度分析6.1 时间复杂度O(S)S所有可行解的长度之和理论最坏情况O(N^(T/M))其中N candidates.lengthT targetM min(candidates)实际运行远小于理论值因剪枝大幅减少搜索空间举例candidates [2],target 40只有一个解[2,2,...,2]20个2时间复杂度 O(20) O(target)6.2 空间复杂度O(target)递归栈深度最坏情况下为target / min(candidates)例如min2,target40→ 深度 20路径存储O(target)当前组合的最大长度结果集空间O(S)但通常不计入空间复杂度标准答案的空间复杂度为 O(target)仅考虑算法运行所需额外空间。七、常见问题解答FAQQ1: 为什么不用动态规划答DP 适合求解的数量或最优解但本题要求输出所有解DP 无法记录具体组合回溯天然适合枚举所有解Q2: 能否用 BFS 实现答可以但效率较低队列存储(current_sum, current_combination, start_index)每次扩展尝试添加candidates[i]i ≥ start_index缺点内存开销大需存储中间组合Q3: 如果 candidates 有重复元素怎么办答这是LeetCode 40组合总和 II需先排序在“跳过”分支后跳过所有重复元素关键代码if(istartcandidates[i]candidates[i-1])continue;Q4: 如何限制每个元素的使用次数答这是0-1 背包或多重背包问题0-1 背包每个元素最多用 1 次 → 递归时start i1多重背包每个元素有使用上限 → 需额外参数记录使用次数八、优化思路8.1 排序 强剪枝已实现预排序Arrays.sort(candidates)循环内 breakif (candidates[i] target) break;8.2 预计算最小值提前终止明显无效的输入intminArrays.stream(candidates).min().getAsInt();if(mintarget)returnresult;// 无解但target ≥ 1且candidates[i] ≥ 2此优化收益有限。8.3 使用数组代替 List微优化减少对象创建privatevoidbacktrack(int[]candidates,inttarget,intstart,int[]path,intlen,ListListIntegerresult){if(target0){result.add(Arrays.stream(path,0,len).boxed().collect(Collectors.toList()));return;}for(intistart;icandidates.length;i){if(candidates[i]target)break;path[len]candidates[i];backtrack(candidates,target-candidates[i],i,path,len1,result);}}调用int[]pathnewint[target];// 最大可能长度backtrack(candidates,target,0,path,0,result);优点无 add/remove 开销缺点代码复杂且需转换为 List8.4 尾递归优化不可行Java 不支持尾递归优化且本题非尾递归递归后还有操作九、数据结构与算法基础知识点回顾9.1 回溯算法核心三要素路径已做出的选择当前组合选择列表当前可做的选择从 start 开始的元素结束条件找到有效解或确定无解模板voidbacktrack(路径,选择列表){if(满足结束条件){result.add(路径);return;}for(选择:选择列表){做选择;backtrack(路径,新选择列表);撤销选择;}}9.2 完全背包 vs 0-1 背包特性完全背包本题0-1 背包元素使用次数无限次最多 1 次递归索引保持不变 (i)递增 (i1)DP 状态转移dp[i][j] dp[i-1][j] dp[i][j-w[i]]dp[i][j] dp[i-1][j] dp[i-1][j-w[i]]9.3 剪枝Pruning技术定义提前终止无效搜索分支类型可行性剪枝当前路径已不可能达到目标如target 0最优性剪枝当前路径已不如已知最优解本题不适用重复性剪枝避免生成重复解通过start参数9.4 组合 vs 排列问题类型是否有序典型题号组合否LeetCode 39, 40, 77排列是LeetCode 46, 47组合问题的关键通过起始索引控制选择顺序避免重复。十、面试官提问环节模拟Q1: 请手写组合总和并解释如何避免重复组合。答写出优化版代码后解释我使用start参数控制选择范围每次递归只考虑从当前索引开始的元素这样确保组合按非递减顺序生成天然避免重复例如先选 2 后可选 2,3,6,7跳过 2 后选 3就不再能选 2Q2: 时间复杂度是多少能否优化答时间复杂度 O(S)S 为所有解的长度之和无法优化渐进复杂度因为必须输出所有解但可通过排序剪枝大幅减少常数因子例如排序后一旦candidates[i] target就终止循环Q3: 如果 candidates 包含负数怎么办答问题会变得复杂可能存在无限解如[-1,1],target0→[-1,1],[-1,-1,1,1], …需要额外约束如限制组合长度或题目保证无负数本题已保证Q4: 如何修改代码以限制每个元素最多使用 k 次答方法1传递使用次数数组在递归时检查方法2展开 candidates如元素 x 最多用 k 次 → 添加 k 个 x方法3在递归参数中加入剩余使用次数// 方法1示例privatevoidbacktrack(int[]candidates,int[]counts,inttarget,intstart,ListIntegerpath,ListListIntegerresult){if(target0){/* ... */}for(intistart;icandidates.length;i){if(counts[i]0candidates[i]target){path.add(candidates[i]);counts[i]--;backtrack(candidates,counts,target-candidates[i],i,path,result);counts[i];path.remove(path.size()-1);}}}Q5: 回溯中的“撤销选择”为什么重要答因为回溯是共享状态的递归。若不撤销后续分支会基于错误的状态计算例如第一次选择 2 后未移除第二次选择 3 时路径变成[2,3]而非[3]导致结果错误或重复十一、这道算法题在实际开发中的应用11.1 货币找零问题给定面额[1,5,10,25]找零target分求所有可能的找零组合注意实际找零通常求最少硬币数用 DP但某些场景需枚举所有方案11.2 资源分配服务器有多种资源配置方案客户需求为target单位资源枚举所有满足需求的配置组合11.3 测试用例生成API 参数有多个选项每个选项可重复使用生成所有参数组合使总“权重”等于目标值11.4 游戏开发技能组合不同技能消耗不同 MP总 MP 为target枚举所有可行的技能释放序列组合11.5 财务规划投资组合不同产品投资额固定总预算为target枚举所有投资方案 虽然实际中很少直接枚举所有组合因数量可能爆炸但组合优化思想广泛应用于各种资源分配问题。十二、相关题目推荐题号题目难度关联点40组合总和 II中等含重复元素需去重216组合总和 III中等限定组合大小 元素范围377组合总和 IV中等求排列数顺序不同算不同77组合中等固定大小组合78子集中等无目标和的组合46全排列中等有序组合518零钱兑换 II中等完全背包计数DP 解法322零钱兑换中等完全背包最值DP 解法494目标和中等正负号组合0-1 背包416分割等和子集中等0-1 背包判定学习路径建议掌握本题完全背包 回溯→ 40去重技巧→ 216加约束条件→ 377排列 vs 组合→ 518/322DP 解法对比十三、总结与延伸13.1 核心收获组合总和是完全背包问题的回溯解法start参数是避免重复组合的关键排序 剪枝能大幅提升性能时间复杂度由输出规模决定无法优化渐进复杂度13.2 延伸思考动态规划能否输出所有解理论上可以但需额外存储路径信息空间开销巨大每个状态需存储所有到达该状态的路径实际不可行本题更适合回溯支持小数或浮点数不推荐浮点精度问题会导致target 0判断失效解决方案使用误差范围Math.abs(target) 1e-6但题目保证整数无需考虑并行化困难回溯的决策树难以分割可能方案对第一个元素的选择并行处理但小数据量下 overhead 大于收益13.3 学习建议动手实现手写回溯模板路径/选择/结束条件理解剪枝排序如何帮助剪枝区分组合/排列通过起始索引控制刷相关题巩固背包问题的不同变种思考 DP vs 回溯何时用哪种方法结语组合总和问题完美展示了回溯算法在组合优化中的强大能力。通过巧妙的剪枝和状态控制我们不仅能高效解决问题还能避免常见的陷阱如重复组合。掌握它不仅是为了解决这一道题更是为了建立起解决更复杂资源分配问题的能力。希望本文能助你在算法之路上走得更远