leetcode题解-Combination Sum系列

这个系列一共有四道题,每道题目之间稍微有些不同,下面通过对比来总结一下,四道题目都可以使用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

  1. Combination Sum II
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];
    }

你可能感兴趣的:(leetcode刷题)