算法练习day21

48 阅读2分钟

一、回溯理论基础

什么是回溯法

回溯是一种搜索的方式,回溯是递归的副产品,只要有递归就有回溯,所以回溯函数就是递归函数

回溯的效率

回溯本质上是暴力穷举,如果想让回溯高效,可以加入剪枝操作

回溯解决的问题

  1. 组合问题,n个数里,按一定规则找出k个数的集合
  2. 切割问题,按照一定规则,字符串有几种切割方式
  3. 子集问题,一个n个数的集合,有多少符合条件的子集
  4. 排列问题,n个数,按一定规则全排列,有几种排列方式
  5. 棋盘问题,n皇后,解数独等

回溯法模板

回溯法的解决的问题,可以抽象为树形结构,集合的大小构成树的宽度,递归的深度构成树的深度

回溯三部曲:

  1. 确定回溯函数的返回值和参数
  2. 回溯函数的终止条件
  3. 回溯函数的遍历过程
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
};