简介
本篇文章介绍「组合总和」各种变形题的解题思路,争取能够攻克所有组合数问题。
题目
39. 组合总和
40. 组合总和 II
216. 组合总和 III
377. 组合总和 Ⅳ
下面通过例子由浅至深详细讲解。
所有例题均来自leetcode,所示代码均通过所有测试。
解析
39. 组合总和
题目描述
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
提示:
- 1 <= candidates.length <= 30
- 1 <= candidates[i] <= 200
- candidate 中的每个元素都是独一无二的。
- 1 <= target <= 500
分析
- 回溯。这类题目很容易想到回溯解法,尝试所有的组合,并将满足条件的加入结果集。
在回溯过程中注意剪枝,减少不必要的尝试。
代码
class Solution {
List<List<Integer>> res;
int target;
int[] candidates;
List<Integer> tmp;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
res = new ArrayList<>();
this.target = target;
this.candidates = candidates;
tmp = new ArrayList<>();
dfs(0, 0);
return res;
}
private void dfs(int i, int total) {
if (total == target) {
res.add(new ArrayList<>(tmp));
return;
}
if (i >= candidates.length) {
return;
}
for (int k = i; k < candidates.length; k++) {
if (candidates[k] + total <= target) {
tmp.add(candidates[k]);
dfs(k, total + candidates[k]);
tmp.remove(tmp.size() - 1);
}
}
}
}
上面代码中 “if (candidates[k] + total <= target)” 就是为了剪枝,只进行必要的递归,不必要的递归会浪费递归调用栈。
40. 组合总和 II
题目描述
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
分析
- 回溯。
和上题区别在于不用重复使用数字,所以递归过程中每次递归需要将索引加1。同时为了避免组合中存在重复的组合,需要先将候选数组排序,然后遍历时发现和上次遍历的数字相同,则不进行递归。
代码
class Solution {
List<List<Integer>> res;
List<Integer> tmp;
int target;
int[] candidates;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
res = new ArrayList<>();
tmp = new ArrayList<>();
this.target = target;
Arrays.sort(candidates);
this.candidates = candidates;
dfs(0, 0);
return res;
}
private void dfs(int i, int total) {
if (total == target) {
res.add(new ArrayList<>(tmp));
return;
}
for (int k = i; k < candidates.length; k++) {
// 避免重复组合
if (k > i && candidates[k] == candidates[k-1]) {
continue;
}
int sum = total + candidates[k];
if (sum <= target) {
tmp.add(candidates[k]);
dfs(k + 1, sum);
tmp.remove(tmp.size() - 1);
}
}
}
}
216. 组合总和 III
题目描述
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
分析
- 回溯。
这道题限制了组合中数字的个数,可以加在剪枝条件中。
代码
class Solution {
List<List<Integer>> res;
List<Integer> tmp;
int target;
int k;
public List<List<Integer>> combinationSum3(int k, int n) {
res = new ArrayList<>();
tmp = new ArrayList<>();
target = n;
this.k = k;
dfs(1, 0, 0);
return res;
}
private void dfs(int i, int total, int count) {
if (count == k && total == target) {
res.add(new ArrayList<>(tmp));
return;
}
if (count == k) {
return;
}
for (int j = i; j < 10; j++) {
int sum = total + j;
if (sum > target || count + 1 > k || sum == target && count + 1 < k || sum < target && count + 1 == k) {
continue;
}
tmp.add(j);
dfs(j + 1, sum, count + 1);
tmp.remove(tmp.size() - 1);
}
}
}
377. 组合总和 Ⅳ
题目描述
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 1000
- nums 中的所有元素 互不相同
- 1 <= target <= 1000
分析
- 回溯。延续前面三道题的思想,很容易想到回溯。但是这道题继续使用回溯会超时不通过。
观察提示中给定的数据长度,比第一题的“candidates.length <= 30”大了一个量级。而回溯的时间复杂度是阶乘级别的,可想而知继续使用回溯肯定超时了。 - 动态规划。和上面三道题不同之处是这道题只需要求解组合的数量,而组合具体是什么不需要求解。
定义dp[x],表示组合之和为x的组合数,那么 d p [ x ] = ∑ n u m : n u m s d p [ x − n u m ] dp[x] = \sum_{num:nums} dp[x - num] dp[x]=∑num:numsdp[x−num]。这是一道类似的背包问题题目。即需要装价值为target的物品,有多少中不同的装法。
时间复杂度:两次遍历,O(kn),k为nums长度,n=target。相比回溯解法,时间上优化了太多。详细见下面代码。
关于动态规划的题目详解,可以参考这篇文章。
代码
class Solution {
public int combinationSum4(int[] nums, int target) {
// dp[i] 表示和为i的种类数
int n = nums.length;
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int num: nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
}