前端重拾算法数据结构一个月(10)

108 阅读3分钟

第十天

昨天了解了什么是回溯法,回溯法的通用场景以及模板。今天来学习回溯算法中组合问题的剪枝操作。在开始前,先来复习一下昨天学的知识,做一道算法题LeetCode.77题。

第二十一题

LeetCode77.组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。

示例:

输入: n = 4, k = 2
输出:
[ [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4] ]
提示:1 <= n <= 20
           1 <= k <= n

按照着昨天的思路开始写吧!

function combine(n: number, k: number): number[ ][ ] {
    const path:number[ ] = [ ]
    const result:number[ ][ ] = [ ]
    const backtracking = (n: number,k: number,startIndex: number)=>{
        if(path.length===k){
            return result.push(path)
        }
        for(let i = startIndex;i<=n;i++){
            path.push(i)
            backtracking(n,k,i+1)
            path.pop()
        }
    }
    backtracking(n,k,1)

    return result
};

结果出事了。居然返回的是[[],[],[],[]]这样的数组。于是我开始疑惑,在控制台里演示,发现了问题!问题就在终止条件中的

return result.push(path)

这个地方是把path推入result中,而后面path的pop()操作和push()操作都会影响到已经被推入到result中的path,因为它们的地址是一样的。所以得推进去一个相同的数组但不能是他本身,而我的第一想法就是用map,因为map会返回一个新的数组,像这样:

return result.push(path.map((i) => i))

但是看到map我又会担心,如果这个数非常大的话,会很麻烦,所以决定用Map来装path:

function combine(n: number, k: number): number[ ][ ] {
    const path:number[ ] = [ ]
    const result:number[ ][ ] = [ ]
    const map:Map<number,number> = new Map()
    const backtracking = (n: number,k: number,startIndex: number)=>{
        if(path.length===k){
            return result.push(Array.from(map.values()))
        }
        for(let i = startIndex;i<=n;i++){
            path.push(i)
            map.set(path.length,i)
            backtracking(n,k,i+1)
            path.pop()
        }
    }
    backtracking(n,k,1)

    return result
};

但是实际上用起来其实没差多少。。。于是不死心的我想着那干脆不要path,直接用Map,想写得更快:

function combine(n: number, k: number): number[ ][ ] {
    const result:number[ ][ ] = [ ]
    const map:Map<number,number> = new Map()
    const backtracking = (n: number,k: number,startIndex: number)=>{
        if(map.size===k){
            return result.push(Array.from(map.values()))
        }
        for(let i = startIndex;i<=n;i++){
            map.set(path.length,i)
            backtracking(n,k,i+1)
            map.delete(map.size)
        }
    }
    backtracking(n,k,1)

    return result
};

写出来了很开心,结果更慢了QAQ空间占用也更大,哭了pass掉。ok那么这一题结束,接下来学习组合问题的剪枝操作。

剪枝操作

还是上面的组合问题,但我们把n=4,k也=4。这样很明显我们可以知道,只有[1,2,3,4]这一种组合是符合要求的。但是像我们在上面的回溯方法中呢,不仅会走1的搜索路径,还会分别走2,3,4的搜索路径。但我们知道只有走1的搜索路径才能满足条件,所以我们可以把2,3,4的搜索路径剪枝。

在昨天讲到了回溯三部曲。分别是:1、确定递归函数的参数以及返回值。2、确定终止条件。3、确定单层搜索(递归)的逻辑。

那么剪枝操作其实是优化了第三步,单层搜索的逻辑。首先回顾出第三步的时候的模板代码,也就是for循环的部分:

for(i=startIndex;i<=n;i++){ 
    path.push(i) 
    backtracking(n,k,i+1)  
    path.pop() 
}

通过上面的例题n=4,k=4可以知道,是只有i=1的情况下可以满足要求,那么我们可以通过在for循环的范围限制的地方做限制来达到目的。而我们想要的范围是:n-i+1=k(+1是因为第一个数也要算在内)4-1+1=4。而在这里面也有一个变量,那就是path,因为for循环是出于回溯中,path中有可能已经存有元素,而path每存在一个元素,i也会随之加一。所以其实范围应变为:n-(i-path.length)+1=k转化为n-i+path.length+1=k,而在for循环中因为是i在左边的表达式,所以再进行移动:i=n-k+path.length+1:

for(i=startIndex;i<=n-k+path.length+1;i++){ 
    path.push(i) 
    backtracking(n,k,i+1)  
    path.pop() 
}

这样就是剪枝操作后的代码了! 很开心,ok那么今天先学到这里,吃个夜宵赶快睡觉了。晚安。