第十天
昨天了解了什么是回溯法,回溯法的通用场景以及模板。今天来学习回溯算法中组合问题的剪枝操作。在开始前,先来复习一下昨天学的知识,做一道算法题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那么今天先学到这里,吃个夜宵赶快睡觉了。晚安。