一、题目描述
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates =[2,3,6,7],
target =7
输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 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 <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
二、解题思路
1. 排序:首先对candidates
数组进行排序,这样可以在后续的回溯过程中进行剪枝,避免重复选择相同的数字。
2. 递归:使用递归函数,每次选择一个候选数字,并从数组中移除(或者标记已使用),然后递归地寻找剩余目标值的组合。
3. 剪枝:
- 当前候选数字与上一个数字相同,并且当前路径的和已经达到了目标值,说明这是一个重复的组合,直接跳过这次递归。
- 如果当前候选数字大于剩余的目标值,说明无法再添加这个数字,直接返回。
4. 记录结果:当递归到目标值减至0时,将当前的组合添加到结果列表中。
5. 回溯:在递归结束后,将当前的候选数字放回原位(如果是从数组中移除的话),以便它可以被上一级递归使用。
三、具体代码
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> results = new ArrayList<>();
if (candidates == null || candidates.length == 0) {
return results;
}
// 对数组进行排序
Arrays.sort(candidates);
backtrack(candidates, target, 0, new ArrayList<>(), results);
return results;
}
private void backtrack(int[] candidates, int remain, int start, List<Integer> combination, List<List<Integer>> results) {
if (remain < 0) {
// 如果当前剩余值小于0,则说明当前组合无法形成解,直接返回
return;
} else if (remain == 0) {
// 如果当前剩余值为0,则说明找到了一个有效的组合,将其添加到结果中
results.add(new ArrayList<>(combination));
return;
}
for (int i = start; i < candidates.length; i++) {
// 剪枝:如果当前数字和上一个数字相同,且已经达到目标值,则跳过
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
// 选择当前数字,并递归寻找剩余值的组合
combination.add(candidates[i]);
backtrack(candidates, remain - candidates[i], i, combination, results);
// 回溯:将当前数字从组合中移除,以便它可以被上一级递归使用
combination.remove(combination.size() - 1);
}
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
-
排序:对
candidates
数组进行排序的时间复杂度是O(n log n),其中n是数组的长度。 -
回溯搜索:在最坏情况下,回溯算法会探索所有可能的组合。对于每个候选数字,我们有两种选择:包含它或者不包含它。因此,对于每个数字,我们都需要进行一次完整的递归搜索。如果数组中的数字可以重复使用,那么对于每个数字,我们可能会重复探索相同的子问题。在最坏的情况下,这会导致时间复杂度接近O(2^n),其中n是数组中不同数字的种类数。然而,由于我们在每一步都尽可能地减少搜索空间(通过剪枝),实际的执行时间通常会比这个最坏情况要好得多。
-
结果集合:我们需要将所有有效的组合存储在结果集合中,最多可能有少于150个组合,所以这部分的时间复杂度是O(k),其中k是有效组合的数量。
-
综上所述,算法的总体时间复杂度由排序和回溯搜索两部分决定。排序是一次性的,而回溯搜索的时间复杂度可能会非常高,但由于剪枝,实际执行时间通常不会达到最坏情况的O(2^n)。结果集合的构建时间复杂度是O(k),其中k是有效组合的数量。
-
因此,我们可以大致估计算法的时间复杂度为O(n log n + k),其中n是数组长度,k是有效组合的数量。
2. 空间复杂度
-
递归栈:在最坏情况下,递归的深度可以达到数组长度,因为我们需要对每个数字进行选择或不选择的决策。因此,递归栈的空间复杂度是O(n)。
-
结果集合:我们需要存储所有有效的组合,所以结果集合的空间复杂度是O(k),其中k是有效组合的数量。
-
综上所述,算法的总体空间复杂度取决于递归栈的深度和结果集合的大小,即O(n + k)。
五、总结知识点
-
数组排序:使用
Arrays.sort
方法对输入的整数数组进行排序。这是为了在后续的回溯过程中能够更有效地进行剪枝操作。 -
递归:
backtrack
函数是一个递归函数,它模拟了组合问题的求解过程。递归是一种通过将问题分解为更小的子问题来解决问题的方法。 -
回溯:回溯是递归的一种特殊形式,它不仅涉及深入问题空间,还涉及从问题空间中退出。在这个问题中,当发现当前的部分解不可能导致最终解时,算法会“回溯”到上一个决策点,尝试其他的选择。
-
剪枝:为了避免无效搜索,代码中使用了剪枝技术。在这个问题中,如果当前候选数字与上一个数字相同,并且已经达到目标值,或者如果当前数字大于剩余的目标值,就会跳过这次递归,从而避免了不必要的搜索。
-
动态规划的思想:尽管代码中没有直接使用动态规划,但是回溯算法在某种意义上与动态规划有相似之处,特别是在处理组合问题时。两者都试图通过构建问题的解的结构来解决问题。
-
数据结构:代码中使用了
ArrayList
来存储中间结果和最终结果。ArrayList
是一种动态数组,可以在运行时动态地添加和删除元素。 -
条件判断:在
backtrack
函数中,通过if
语句进行条件判断,以确定是否应该继续搜索或者返回。 -
List的不可变性:在将组合添加到结果列表
results
时,代码创建了一个新的ArrayList
,这是为了避免修改原始的combination
列表,因为List
在Java中是不可变的。 -
空间复杂度优化:通过在回溯过程中从
combination
列表中移除元素而不是创建新的列表,优化了空间复杂度。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。