题目描述
给定一个无重复元素的数组 candidates
和一个目标值 target
,找出 candidates
中所有可以使数字和为 target
的组合。数组中的数字可以被重复使用。
示例:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
代码解析
class Solution {
// 记录当前的组合路径
public List<Integer> path = new ArrayList<>();
// 存储所有符合条件的组合结果
public List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 先排序,方便后续剪枝
Arrays.sort(candidates);
// 调用回溯函数,从索引0开始
dfs(candidates, 0, target, 0);
return ans;
}
public void dfs(int[] candidates, int sum, int target, int i) {
// 当路径和等于目标值时,将当前路径加入结果集
if (sum == target) {
ans.add(new ArrayList<>(path));
return;
}
// 遍历候选数组,从索引 i 开始,保证组合不重复
for (int j = i; j < candidates.length; j++) {
// 剪枝:如果当前元素加入后超过目标值,则终止循环
if (sum + candidates[j] > target) {
break;
}
// 选择当前元素
sum += candidates[j];
path.add(candidates[j]);
// 递归调用,继续尝试选择当前元素(允许重复使用)
dfs(candidates, sum, target, j);
// 回溯:撤销当前选择,恢复 sum 和 path 状态
path.remove(path.size() - 1);
sum -= candidates[j];
}
}
}
代码详解
-
全局变量
-
path
用于存储当前的组合路径。 -
ans
用于存储所有满足条件的组合。
-
-
排序
-
Arrays.sort(candidates);
使数组有序,方便后续剪枝。
-
-
深度优先搜索(DFS)
-
sum
记录当前路径和。 -
i
控制遍历起点,确保组合不重复。 -
若
sum == target
,将当前path
加入ans
。 -
剪枝:若
sum + candidates[j] > target
,提前终止循环。 -
递归时,
j
位置不变,允许重复使用当前数字。 -
回溯时,撤销选择,保证
sum
还原。
-
复杂度分析
-
时间复杂度:最坏情况下,递归树的深度为
target/min(candidates)
,每层最多n
个分支,时间复杂度约为O(n^k)
。 -
空间复杂度:
O(k)
,k
为递归深度。
优化
-
剪枝:提前排序并在
sum + candidates[j] > target
时终止循环。 -
回溯优化:
sum
变量直接参与递归,减少重复计算。
注意
下面是一些需要特别留意的地方:
-
排序与剪枝:
排序操作有助于剪枝,因为排序后如果当前路径和sum
加上某个候选数超过了target
,可以直接终止循环,避免不必要的计算。这种优化能显著提高效率。 -
重复元素问题:
虽然题目给定的是“无重复元素的数组”,但我们仍然要小心避免不同的组合产生重复结果。在回溯中通过从当前位置i
开始递归,可以确保每个组合只出现一次。 -
回溯时的恢复:
在回溯过程中,每次选择一个数字后都要撤销该选择(即path.remove
和sum -= candidates[j]
)。这一步是保证回溯算法能够正常工作的关键,因为它允许探索所有可能的路径。