小白也能看懂的回溯算法剪枝

66 阅读3分钟

力扣40题是一道经典的题,需要算出一个数组里等于某个target值的数组组合,并且每个数字只能用一次。

leetcode.cn/problems/co…

比如[2,3,3,5,6]要拼出8。

我们是通过回溯算法进行推演的,把某个元素push进数组,然后判断数组里的元素之和是否恰好等于我们的target。 如果加起来超过了,那说明我们最后一次的值太大了,我们需要往上回退一层。尝试下一个数。

我们用startIdx表示我们以哪个idx作为我们的主干(这里用来表示所处的层次)。

比如第一次startIdx= 0,我们先把2作为主干。

然后不断向下查找主干,看看有没有符合要求的节点。

此时path = [2],然后看看是否小于target,如果是我们就去找2后面的元素,看看有没有组合的值为6。

接着继续让startIdx+1,表示我们继续向下分支查找,path为[2,3]

image.png

此时还是小于8,继续找,直到path为[2,3,3] 。 此时startIdx = 2,我们在第三层主干。刚好等于8。结束。

image.png

然后我们还需要继续遍历startIdx为2这一层,发现把5加进来后发现path为[2,3,5]。

超过8了,后面的都不需要判断了。此时我们直接回到startIdx = 1这一层。

这时候是重点,第二个节点也是3,这里我们就需要剪枝了,因为这个3作为一个分支时,是与第一个3重复的。(这里的剪枝是主要的难点)

那这里要怎么考虑呢?实际就是我们只需要在每一层组合数据时,相同的分支要去掉。

所以就是从第二个节点开始,判断这个节点是否和前一个相同(已经排序了),如果相同了,都过滤掉。

这里的条件就是

i > startIdx && candidates[i] == candidates[i - 1]

比如我们在第二层,里面第二个3,就是i = 2(i表示我们在每一层遍历的分支节点),此时说明他和前一个3重复了,当第一个3去组合其他数据时,会和它重复,所以我们直接过滤掉。

image.png

然后我们继续找[2,5]这个组合,发现后面加上6也超了,直接忽略

image.png

继续回退到startIdx = 1这一层,尝试[2,6]组合,发现符合要求

image.png

以其他数为根节点的逻辑过程可以用同样的方法推理。

完整代码如下

var combinationSum2 = function(candidates, target) {
    candidates.sort((a,b)=>a-b)
    const sum = arr=>arr.reduce((acc,cur)=>acc+cur,0)
    const result = []
    const backTrack = (start,path)=>{
        if(sum(path) === target){
            result.push([...path])
            return
        }
        for(let i=start;i<candidates.length;i++){
             // 与两数和类似,因为是排序了,两个相邻的一样数向下查找若存在解则解包含
            if (i > start && candidates[i] == candidates[i - 1]) {
                // 跳过相同的数值
                continue; 
            }
            if(sum([...path,candidates[i]])<=target){
                path.push(candidates[i])
                backTrack(i+1,path)
                path.pop()
            }
        }
    }
    backTrack(0,[])
    return result
};