持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情
前言
今天的题目为中等,主要是用于重新的学习一下回溯算法,并且按照特定的解题技巧来解决一下回溯,之后可能会专门训练一下回溯的各种题型。
每日一题
今天的题目是 77. 组合,难度为中等
-
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
-
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
- 1 <= n <= 20
- 1 <= k <= n
题解
回溯
题目属于很经典的回溯算法题,回溯算法的本质就是递归,并且回溯的本质就是暴力搜索,除了能够通过一些剪支来进行优化,但还是改变不了暴力搜索的本质。
至于题目为什么需要使用回溯来进行解答,我们拿例子来说明一下,例子一中,n = 4, k = 2,那么说明是 [1,2,3,4] 这个数组需要寻找 长度为 2 的组合,那么在这种情况下我们可以通过双重循环来获取每一个元素的两两组合情况,最后返回答案。
for(let i=0;i<n;i++){
for(let j=0;j<n;j++){
...
}
}
这种做法固然可以解决当前这种情况,但是万一k为3呢?那就3个for循环,4就4个,那么就会发现,这道题的k值会让我们连for循环嵌套的这种暴力写法都解决不了。那么这个时候就轮到回溯登场了。
回溯的本质就是递归。并且每个回溯都可以抽象为一棵树,树的深度就是递归的次数。
那么这道题目可以抽象为下面这棵树:
通过这棵树,我们就能够看出来这道题的一个回溯流程,通过回溯的一个模板解法,就能够完成题目。
function combine(n: number, k: number): number[][] {
let res = []
let path = []
const backtracking = (index) => {
if (path.length == k) {
res.push([...path]);
return;
}
for (let i = index; i <= n; i++) {
path.push(i);
backtracking(i + 1); // 递归
path.pop()
}
return
}
backtracking(1)
return res
};
剪枝
上面就有稍微提到过,通过剪枝我们可以过滤掉一些不必要的分支,就有点像在写for循环的时候,我们可以让无效的循环直接结束,以此来节省运算时间,那么上面这道题我们要怎么进行剪枝呢。通过一幅图来看一下
当 n=4,k=4 的时候,上面的这些计算分支,我们就会发现一些可以直接跳过的地方,比如说 2 后面选中 3 或者 4 这个分支应该是无效分支,因为连k=4这个条件都不可能达成,并且在剩下的过程当中,我们会发现还有很多这样的无效分支,那么对于这些分支,在递归的时候,我们就可以进行判断,从而减少递归的次数。
function combine(n: number, k: number): number[][] {
let res = []
let path = []
const backtracking = (index) => {
if (path.length == k) {
res.push([...path]);
return;
}
for (let i = index; i <= n; i++) {
if(path.length + n-i < k-1) {
continue
} // 剪枝操作
path.push(i);
backtracking(i + 1); // 递归
path.pop()
}
return
}
backtracking(1)
return res
};