一、回溯理论基础
什么是回溯法
回溯是一种搜索的方式,回溯是递归的副产品,只要有递归就有回溯,所以回溯函数就是递归函数
回溯的效率
回溯本质上是暴力穷举,如果想让回溯高效,可以加入剪枝操作
回溯解决的问题
- 组合问题,n个数里,按一定规则找出k个数的集合
- 切割问题,按照一定规则,字符串有几种切割方式
- 子集问题,一个n个数的集合,有多少符合条件的子集
- 排列问题,n个数,按一定规则全排列,有几种排列方式
- 棋盘问题,n皇后,解数独等
回溯法模板
回溯法的解决的问题,可以抽象为树形结构,集合的大小构成树的宽度,递归的深度构成树的深度
回溯三部曲:
- 确定回溯函数的返回值和参数
- 回溯函数的终止条件
- 回溯函数的遍历过程
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
二、组合
startIndex,每向下走一层加1,代表遍历的起始点,防止重复
递归终止条件为path的长度等于k,递归回退后,把path撤销,进入下一轮遍历
剪枝优化,如果遍历的起始点之后的元素不足我们需要的元素时,就没必要继续遍历了
k - path.length,剩余还需要的元素个数
n - (k - path.length) + 1,从起始点(从1开始),到结束节点的位置
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function (n, k) {
let result = []
let path = []
function backtracking(startIndex) {
if (path.length === k) {
result.push([...path])
return
}
for (let i = startIndex; i <= n - (k - path.length) + 1; i++) {
path.push(i)
backtracking(i + 1)
path.pop(i)
}
}
backtracking(1)
return result
};