LeetCode 39 LeetCode 40 组合总和问题详解:回溯算法与剪枝优化(Java实现)
文章目录
- 1. 问题概述
- 2. 组合总和I(无重复元素,允许重复使用)
- 2.1 方法思路
- 2.2 代码实现
- 2.3 复杂度分析
- 3. 组合总和II(包含重复元素,不可重复使用)
- 3.1 方法思路
- 3.2 代码实现
- 3.3 关键点解析
- 3.4 示例分析
- 4. 总结对比
- 5. 常见问题
- Q1:为什么必须排序?
- Q2:去重逻辑为何用`i > start`?
1. 问题概述
组合总和(Combination Sum)是经典的算法问题,常见于面试和编程练习。本文针对两个变体问题进行解析:
-
问题39(组合总和I)
给定无重复元素的整数数组candidates
和目标数target
,找出所有可以使数字和等于target
的唯一组合。数组中的数字可以无限次重复使用。 -
问题40(组合总和II)
给定可能包含重复元素的整数数组candidates
和目标数target
,找出所有可以使数字和等于target
的唯一组合。数组中的每个数字在每个组合中只能使用一次。
2. 组合总和I(无重复元素,允许重复使用)
2.1 方法思路
使用回溯算法遍历所有可能的组合,并通过排序剪枝优化效率:
- 排序预处理:对数组排序,便于后续剪枝。
- 回溯框架:递归遍历数组,允许重复选择同一元素。
- 剪枝优化:当当前路径和超过目标值时提前终止循环。
2.2 代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();Arrays.sort(candidates); // 排序以便剪枝backtrack(candidates, target, 0, new ArrayList<>(), 0, result);return result;}private void backtrack(int[] candidates, int target, int start, List<Integer> path, int sum, List<List<Integer>> result) {if (sum == target) {result.add(new ArrayList<>(path)); // 记录有效组合return;}for (int i = start; i < candidates.length; i++) {int num = candidates[i];if (sum + num > target) break; // 剪枝:后续元素更大,无需继续path.add(num);backtrack(candidates, target, i, path, sum + num, result); // 允许重复使用ipath.remove(path.size() - 1); // 回溯}}
}
2.3 复杂度分析
- 时间复杂度:最坏情况为
O(N^(T/M))
,其中N
为数组长度,T
为目标值,M
为最小元素值。剪枝优化后实际效率更高。 - 空间复杂度:递归栈深度为
O(T/M)
。
3. 组合总和II(包含重复元素,不可重复使用)
3.1 方法思路
在回溯基础上增加去重处理:
- 排序预处理:使相同元素相邻,便于跳过重复。
- 同层去重:在同一递归层级中,跳过与前一个元素相同的候选值。
- 剪枝优化:提前终止无效路径的探索。
3.2 代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;class Solution {public List<List<Integer>> combinationSum2(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();Arrays.sort(candidates); // 必须排序以处理重复元素backtrack(candidates, target, 0, new ArrayList<>(), result);return result;}private void backtrack(int[] candidates, int remain, int start, List<Integer> path, List<List<Integer>> result) {if (remain == 0) {result.add(new ArrayList<>(path)); // 记录有效组合return;}for (int i = start; i < candidates.length; i++) {// 跳过同一层中的重复元素if (i > start && candidates[i] == candidates[i - 1]) continue; if (candidates[i] > remain) break; // 剪枝:后续元素更大path.add(candidates[i]);backtrack(candidates, remain - candidates[i], i + 1, path, result); // i+1避免重复使用path.remove(path.size() - 1); // 回溯}}
}
3.3 关键点解析
- 去重逻辑:
if (i > start && candidates[i] == candidates[i-1])
i > start
确保只在同层级跳过重复元素(如第一层循环),允许不同层级使用相同值(如[1,1,6]
)。
- 剪枝优化:由于数组已排序,当
candidates[i] > remain
时,后续元素必然无效,直接终止循环。
3.4 示例分析
以输入candidates = [1,1,2,5,6,7,10], target = 8
为例:
- 第一层选第一个
1
,进入递归remain=7
。 - 第二层从索引
1
开始,跳过第二个1
(i=1 == start=1
,不触发跳过),选2
后继续递归。 - 有效组合包括
[1,1,6]
、[1,2,5]
等,确保无重复。
4. 总结对比
问题特性 | 组合总和I | 组合总和II |
---|---|---|
候选数组元素 | 无重复 | 可能包含重复 |
元素使用规则 | 可重复使用 | 不可重复使用 |
核心处理 | 剪枝优化 | 剪枝 + 同层去重 |
时间复杂度 | O(N^(T/M)) | O(2^N) |
5. 常见问题
Q1:为什么必须排序?
排序是实现剪枝和去重的前提条件:
- 剪枝:有序数组可提前终止无效路径。
- 去重:相同元素相邻,便于跳过重复。
Q2:去重逻辑为何用i > start
?
i > start
确保只在同一层级跳过重复元素,例如:当start=0
且i=1
时,若candidates[1] == candidates[0]
,则跳过第二个1
,避免生成[1,1,6]
和[1,1,6]
的重复组合。
通过本文,可以掌握使用回溯算法解决组合总和问题的核心技巧,理解剪枝与去重的实现原理,并能够举一反三处理类似问题。