【算法27天:Day27】第七章回溯算法 LeetCode 组合总和(39)

71 阅读1分钟

题目一:

image.png

解法一:(回溯)

解题思路:这个题我并没有传入startIndex,因为我想的是是可以重复的,然后每次循环i都等于0,这样就会有[2, 3, 2]这样的结果加入到result中,然后我在sum === target时,进行判断,如果当前path中后一个数比前一个数小,那么就不加入,这样就可以去除[2, 3, 2],这样也是可以通过的。

var combinationSum = function(candidates, target) {
    let result = []
    let path = []
    let sum = 0
    const backtracking = function(candidates, target, sum) {
        if (sum > target) return 
        if (sum === target) {
            for (let i = 0; i < path.length; i++) {
                for (let j = i + 1; j < path.length; j++) {
                    if (path[j] < path[i]) {
                        return 
                    }
                }
            }
            result.push([...path])
            return 
        }
        for (let i = 0; i < candidates.length; i++) {
            sum += candidates[i]
            path.push(candidates[i])
            backtracking(candidates, target, sum)
            path.pop()
            sum -= candidates[i]
        }
    }
    backtracking(candidates, target, sum)
    return result
};

回溯三部曲:

  • 递归函数参数

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入)

首先是题目中给出的参数,集合candidates, 和目标值target。

此外还定义了sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)216.组合总和III (opens new window)

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)

注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍

代码如下:

const res = [], path = [];
function backtracking(j, sum) 
  • 递归终止条件

在如下树形结构中:

39.组合总和

从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。

sum等于target的时候,需要收集结果,代码如下:

if (sum > target) {
    return;
}
if (sum == target) {
    result.push(path);
    return;
}
  • 单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合。

注意本题和77.组合 (opens new window)216.组合总和III (opens new window)的一个区别是:本题元素为可重复选取的

如何重复选取呢,看代码,注释部分:

for (int i = startIndex; i < candidates.length; i++) {
    sum += candidates[i];
    path.push(candidates[i]);
    backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
    sum -= candidates[i];   // 回溯
    path.pop();        // 回溯
}

完整代码:

var combinationSum = function(candidates, target) {
    let result = []
    let path = []
    const backtracking = function(candidates, target, sum, startIndex) {
        if (sum > target) return 
        if (sum === target) {
            result.push([...path])
            return 
        }
        for (let i = startIndex; i < candidates.length; i++) {
            sum += candidates[i]
            path.push(candidates[i])
            backtracking(candidates, target, sum, i)
            path.pop()
            sum -= candidates[i]
        }
    }
    backtracking(candidates, target, 0, 0)
    return result
};

剪枝优化

在这个树形结构中:

39.组合总和

以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。

那么可以在for循环的搜索范围上做做文章了。

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

如图:

39.组合总和1

for循环剪枝代码如下:

for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++)

总结

本题和我们之前讲过的77.组合 (opens new window)216.组合总和III (opens new window)有两点不同:

  • 组合没有数量要求
  • 元素可无限重复选取

在求和问题中,排序之后加剪枝是常见的套路!

var combinationSum = function(candidates, target) {
    const res = [], path = [];
    candidates.sort((a,b)=>a-b); // 排序
    backtracking(0, 0);
    return res;
    function backtracking(j, sum) {
        if (sum === target) {
            res.push(Array.from(path));
            return;
        }
        for(let i = j; i < candidates.length; i++ ) {
            const n = candidates[i];
            if(n > target - sum) break;
            path.push(n);
            sum += n;
            backtracking(i, sum);
            path.pop();
            sum -= n;
        }
    }
};