这个系列一共有四道题,每道题目之间稍微有些不同,下面通过对比来总结一下,四道题目都可以使用backtracking回溯方法做,当然也可以是使用DP进行求解。首先看第一道:
39. Combination Sum
Given a set of candidate numbers (C) (without duplicates) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
The same repeated number may be chosen from C unlimited number of times.
Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
For example, given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[
[7],
[2, 2, 3]
]
看完题目不难理解,其实就是在一个数组中寻找和为target的组合,数组中不存在重复元素,不过每个元素可以重复使用,但是最终的求解方案不能有重复。我们首先可以将数组进行排序,然后使用回溯法逐个元素求和,当求和等于target的时候将结果保存下来,当求和小于target的时候继续向前遍历,当求和大于target的时候删除当前元素遍历下一个。代码如下所示,可以击败46%的用户:
public List> combinationSum(int[] candidates, int target) {
List> res = new ArrayList<>();
//对数组进行排序
Arrays.sort(candidates);
help(candidates, target, res, new ArrayList(), 0);
return res;
}
public void help(int [] candidates, int target, List> res, ArrayList path, int start){
//当求和小于target的时候,添加当前元素,并继续递归调用。
if(target > 0){
//对当前元素,需要遍历其后面的每个元素,
for(int i=start; i//递归调用的时候,start传入i,主要是因为每个元素可以重复使用
help(candidates, target-candidates[i], res, path, i);
//当调用结束之后,删除最顶的元素
path.remove(path.size()-1);
}
}else if(target == 0)//如果和等于target,则将结果添加到res中
res.add(new ArrayList(path));
}
参考题目中的例子,上述函数的执行过程为:
一直调用得到2,2,2,2和大于8,直接返回,执行remove,然后变成2,2,2,3,再返回直到for循环运行结束变成2,2,3,和正好为7,保存结果,继续遍历至2,2,7,再返回上一层,得到2,3,6,一直执行下去得到最后的7元素并保存。从上面的流程可以看出来,整个过程仍然存在很大的重复和冗余,因为数组已经排序,所以当2,2,2,2不满足条件的时候应该直接结束本次循环,返回上一层到2,2,3。因此做一个判断就可以极大程度的改善程序运行效果,下面代码插入两句话就可以击败95。5%的用户:
public List> combinationSum(int[] candidates, int target) {
List> res = new ArrayList<>();
if(candidates == null || candidates.length == 0)
return res;
Arrays.sort(candidates);
help(candidates, target, res, new ArrayList(), 0);
return res;
}
public void help(int [] candidates, int target, List> res, ArrayList path, int start){
if(target > 0){
for(int i=start; i//如果当前元素大于target,则直接break,不在便利后面的元素即可
if(candidates[i] > target)
break;
path.add(candidates[i]);
help(candidates, target-candidates[i], res, path, i);
path.remove(path.size()-1);
}
}else if(target == 0)
res.add(new ArrayList(path));
}
其实这个题目也可以使用动态规划的方法来求解,主要原因是其需要得到每个解的具体值,而不是只需要返回最终的可行解的个数,这就会导致DP需要使用大量的空间,并且在插入等操作上浪费很多时间,从而导致运行速度很慢。为了精简篇幅这里不再放出DP解法,这部分的解释和代码可以参考下面这个链接:https://leetcode.com/problems/combination-sum/discuss/16631/Dynamic-programming-solution-and-why-DP-is-slow-for-this-problem
Given a collection of candidate numbers (C) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
Each number in C may only be used once in the combination.
Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
本题与上面的区别就是数组中的元素可以重复,但是每个元素不能重复使用,最简单的一个想法是在之前代码的基础之上把递归调用那里的i改成i+1,这样就不会重复使用某一个元素了,但是这样做仍然会存在一个问题,譬如上面的例子,会有两个1,7和1,2,5的结果被保存下来,因为虽然1没有被重复使用,但是数组中本身存在两个1,那相应的结果必然会被保存两次,针对这种情况,首先可以使用Set保存遍历出来的结果,进行去重的方法处理,但是这样做算法的速度会比较低,因为首先还是会有很多重复计算的过程,比如数组中有多少个1,就会重复计算多少次,另外,Set转List也会消耗一些额外的计算量,代码如下所示,可以击败15%的用户:
public static List> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
Set> res = new HashSet<>();
help(candidates, target, res, new ArrayList(), 0);
List> list = new ArrayList<>();
for(List li : res)
list.add(li);
return list;
}
public static void help(int [] candidates, int target, Set> res, ArrayList path, int start){
if(target > 0){
for(int i=start; iif(candidates[i] > target)
break;
path.add(candidates[i]);
help(candidates, target-candidates[i], res, path, i+1);
path.remove(path.size()-1);
}
}else if(target == 0)
res.add(new ArrayList<>(path));
}
为了不使用Se进行转化,我们需要增加一个判断,就是如果当前元素的值与他前面元素的值相同,就应该跳过该元素,直接判断下一个元素,这样在结合之前提出的如果当前元素的值本身已经大于target,我们可以击败98%的用户,代码如下所示:
public List> combinationSum2(int[] cand, int target) {
Arrays.sort(cand);
List> res = new ArrayList>();
List path = new ArrayList();
dfs_com(cand, 0, target, path, res);
return res;
}
void dfs_com(int[] cand, int cur, int target, List path, List> res) {
if (target == 0) {
res.add(new ArrayList(path));
return ;
}
if (target < 0) return;
for (int i = cur; i < cand.length; i++){
if(cand[i] > target)
break;
if (i > cur && cand[i] == cand[i-1]) continue;
path.add(path.size(), cand[i]);
dfs_com(cand, i+1, target - cand[i], path, res);
path.remove(path.size()-1);
}
}
接下来看第三道题目216. Combination Sum III
Find all possible combinations of k numbers that add up to a number n, given that only numbers from 1 to 9 can be used and each combination should be a unique set of numbers.
Example 1:
Input: k = 3, n = 7
Output:
[[1,2,4]]
Example 2:
Input: k = 3, n = 9
Output:
[[1,2,6], [1,3,5], [2,3,4]]
从题目可以看出,稍微做了变化,主要体现在,数组限制在1-9之间,数组本身没有重复数字,每个元素也不能重复使用,很大程度上降低了编程的难度,唯一一个难点在于限制了使用数字的个数为k,也就是说只能k个数求和才算,是不是有种似曾相识kSUM的感觉。这里先不扯他们,单纯从backtracking的角度来看,其实只需要引入一个变量k就可以满足,而且结合我们上面总结出的trick,可以实现100%的击败,代码如下所示:
public static List> combinationSum3(int k, int n) {
List> res = new ArrayList<>();
help(k, n, res, new ArrayList(), 1);
return res;
}
public static void help(int k, int n, List> res, ArrayList path, int start){
if(k == 0 && n == 0)
res.add(new ArrayList<>(path));
else if(k > 0 && n >0){
for(int i=start; i<=9; i++){
if(i > n)
break;
path.add(i);
help(k-1, n-i, res, path, i+1);
path.remove(path.size()-1);
}
}
}
接下来看最后一道题目,377. Combination Sum IV
Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.
Example:
nums = [1, 2, 3]
target = 4
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Note that different sequences are counted as different combinations.
Therefore the output is 7.
Follow up:
What if negative numbers are allowed in the given array?
How does it change the problem?
What limitation we need to add to the question to allow negative numbers?
其实原理跟上面相同,这里数组元素不会重复,而且元素可以重复使用,不单单可以重复使用最终的答案还会考虑数字之间的顺序,只要顺序不同,就算不同答案,譬如2,1,1和1,2,1和1,1,2这样,此外,一个关键的区别就在于本题并不需要返回每个方案的解,而只需要返回解的个数即可,这就使得本题更适合使用DP解法。首先来看一下回溯方法,也很简单,这种方法我在本地测试通过,但是在LeetCode上面老师报错,估计是用了静态变量的问题==:
//定义一个静态变量,作为最终的结果,
static int res = 0;
public static int combinationSum4(int[] candidates, int target) {
Arrays.sort(candidates);
help(candidates, target,0);
return res;
}
public static void help(int [] candidates, int target, int start){
if(target > 0){
for(int i=start; i//注意这里递归调用的时候,传入的索引是start,而不是i,因为要考虑最终答案之间的顺序,所以对每个元素应该从头开始遍历,而不是从i开始
help(candidates, target-candidates[i],start);
}else if(target == 0)
res ++;
}
接下来看一下DP解法,也很简单,不多赘述:
public int combinationSum4(int[] nums, int target) {
Arrays.sort(nums);
int[] res = new int[target + 1];
for (int i = 1; i < res.length; i++) {
for (int num : nums) {
if (num > i)
break;
else if (num == i)
res[i] += 1;
else
res[i] += res[i-num];
}
}
return res[target];
}