文章摘要:
- 本文讨论了LeetCode上组合总和问题的两种回溯解法。第一种解法通过深度优先搜索枚举所有可能的组合,使用剪枝条件避免重复结果(如[2,3]和[3,2]),并通过全局变量记录路径和结果。第二种解法采用分层策略,每层决定是否选择当前数字及其数量,通过循环控制数字选择次数。两种方法都利用了回溯机制恢复现场,确保正确性。最终代码展示了如何在Java中实现这两种策略,其中关键点包括递归终止条件、剪枝处理以及回溯时的路径管理。
文章目录
- 一、题目解析
- [二、算法原理 + 代码实现](#二、算法原理 + 代码实现)
一、题目解析

题目需要让我们从给出的数组 candidates 中找到和为 target 的所有不同组合,每一个数字可以重复选择。
例如示例 1 给出 candidates = [ 2, 3, 6, 7 ],target = 7,则 [ [ 2, 2, 3 ], [ 7 ] ] 就是所有的组合。
示例 2 给出 candidates = [ 2, 3, 5 ],target = 8,则 [ [ 2, 2, 2, 2 ], [ 2, 3, 3 ], [ 3, 5 ] ] 是所有的组合。
二、算法原理 + 代码实现
这道题目同样是一道暴搜题目,大概思路是:先暴力枚举(深度优先遍历)出所有组合,然后根据一些条件剪枝筛选出结果即可。
解法一
决策树
根据示例 2 来画决策树。

我们画决策树的思路是:给出若干个位置,这些位置都可以选所给数组的所有数字,我们的节点是这些位置上所有数字之和,当节点的值等于 target ,记录结果。
这里需要注意不要重复结果,比如 [ 2, 3 ] 和 [ 3, 2 ] 是同一个结果,需要剪枝掉;同时,[ 2, 5 ] 和 [ 5, 2 ] 也是同一个结果。
剪枝条件是:从当前数字开始往后遍历枚举。

全局变量
这里一如既往的是两个全局变量:ret 记录结果,path 记录路径。
本题涉及到求和操作,而且求和可以使用一个基本数据类型来记录,我们就可以借助程序的回溯自动恢复现场这一特性,将原本要作为全局变量的 sum 改为 dfs 函数的参数。
为了方便,将题目接口的参数 target 改成全局变量 aim,这样每一次递归就不用多加一个参数了。
Java
List<List<Integer>> ret;
List<Integer> path;
int aim;
dfs 函数
函数头
这里函数头的设计主要看每一次递归要做什么,每一次递归都是选择 candidates 数组的数字,因此将数组作为参数。同时我们通过递归遍历数组,需要下标 pos 作为参数。还有一个用于记录组合之和的 sum。
Java
dfs(int[] nums, int pos, int sum);
函数体
关注每一次递归做的事情,从当前位置的数字开始往后遍历,用到循环
Java
for (int i = pos; i < nums.length; i++) {
// 将当前数字添加到 path
// 递归
// 回溯恢复现场
}
细节问题
回溯
这里的回溯恢复现场的操作也是将 path 最后一个元素删掉即可。
剪枝
我们在函数体中通过 "从当前数字开始往后遍历" 这一操作已经剪枝了,同时要注意当 sum 大于目标值 aim/target 的时候,也要剪掉,因为已经不符合条件了。
递归出口
当 sum 等于目标值 aim/target 的时候,就更新结果并返回,但是还要注意 pos 是否合法,如果 pos 等于 nums 的边界,就必须返回,否则会越界访问。
代码实现
Java
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int aim;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
ret = new ArrayList<>();
path = new ArrayList<>();
aim = target;
dfs(candidates, 0, 0);
return ret;
}
private void dfs(int[] nums, int pos, int sum) {
if (sum == aim) {
ret.add(new ArrayList<>(path));
return;
}
// 剪枝以及防止越界访问
if (sum > aim || pos == nums.length) return;
// 从当前数字开始往后遍历
for (int i = pos; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i, sum + nums[i]);
path.remove(path.size() - 1); // 回溯时恢复现场
}
}
}
解法二
决策树
同样根据示例 2,

我们的思路是:每一层是选择某一个数字,节点存放组合之和。第一层的一个节点是 0,然后第二层选择数字 2,这时候只考虑选择多少个数字 2,可以是 0 个、1 个、2 个... 4 个,但是不可以 5 个,因为 5 个 2 是 10,已经大于目标值 8 了。然后第三层选择数字 3,只考虑选择多少个数字 3,以此类推,将数组中所有数字都枚举到。

解法二和解法一大部分是相同的,只有函数体部分和回溯部分不太一样。
dfs 函数体 + 细节问题:回溯
这里我们要做的是考虑当前数字的个数,因此就要循环枚举 0 个当前数字的情况、1 个当前数字的情况、2 个当前数字的情况...然后进入下一层。循环条件是:当 i 个当前数字加上当前层的 sum 小于等于目标值的时候可以继续循环,若大于就退出循环枚举。注意当数字取 0 个的时候,实际上并不是真的要添加到 path 中,因此在执行该操作之前要判断以下 i 是否为 0。
!注意,这里递归回来之后不要立即恢复现场,因为我们是要累加当前数字的个数的,也就是同一层选择数字个数这里不要恢复现场,当选择数字的循环结束后,再将之前添加的所有元素删掉,也是通过一个循环来操作,但是由于数字取 0 个的情况实际上并没有真的添加进 path 中,因此循环从数字取 1 个的情况开始。
Java
for (int i = 0; i * nums[pos] + sum <= aim; i++) {
// 添加当前数字到 path 中
// 递归去下一层
}
// 回溯恢复现场
for (int i = 1; i * nums[pos] + sum <= aim; i++) {
// 删除最后的元素
}
代码实现
Java
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int aim;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
ret = new ArrayList<>();
path = new ArrayList<>();
aim = target;
dfs(candidates, 0, 0);
return ret;
}
private void dfs(int[] nums, int pos, int sum) {
if (sum == aim) {
ret.add(new ArrayList<>(path));
return;
}
// 剪枝以及防止越界访问
if (sum > aim || pos == nums.length) return;
// 考虑每个数字使用多少次
for (int i = 0; i * nums[pos] + sum <= aim; i++) {
if (i != 0) path.add(nums[pos]);
dfs(nums, pos + 1, i * nums[pos] + sum); // 递归下一层
}
// 回溯时恢复现场
for (int i = 1; i * nums[pos] + sum <= aim; i++) {
path.remove(path.size() - 1);
}
}
}
文章到这里就告一段落啦,若有错误请尽管指出~
完