回溯算法总结(JavaScript实现)

概念

维基百科中对于回溯算法的定义:javascript

回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程当中,当它经过尝试发现现有的分步答案不能获得有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再经过其它的可能的分步解答再次尝试寻找问题的答案。回溯法一般用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种状况:java

  • 找到一个可能存在的正确的答案;
  • 在尝试了全部可能的分步方法后宣告该问题没有答案

关键词:深度优先、递归、尝试各类可能、回退、遍历(暴力解法)算法

回溯与动态规划的区别

共同点:编程

用于求解多阶段决策问题。多阶段决策问题即:数组

  • 求解一个问题分为不少步骤(阶段);
  • 每个步骤(阶段)能够有多种选择。

不一样点:markdown

  • 动态规划只须要求咱们评估最优解是多少,最优解对应的具体解是什么并不要求。所以很适合应用于评估一个方案的效果;
  • 回溯算法能够搜索获得全部的方案(固然包括最优解),可是本质上它是一种遍历算法,时间复杂度很高。

相关经典算法题

鲁迅说过,算法学习,七分练,三分学。接下来就让咱们一块儿来康康常见的几个题目吧~学习

全排列

leetcode 46:ui

输入: [1,2,3]
输出: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]编码

回溯算法思路: 说明:spa

  • 每个结点表示了求解全排列问题的不一样的阶段,这些阶段经过变量的「不一样的值」体现,这些变量的不一样的值,称之为「状态」;
  • 使用深度优先遍历有「回头」的过程,在「回头」之后, 状态变量须要设置成为和先前同样 ,所以在回到上一层结点的过程当中,须要撤销上一次的选择,这个操做称之为「状态重置」;
  • 深度优先遍历,借助系统栈空间,保存所须要的状态变量,在编码中只须要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的作法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,须要撤销上一次的选择,也是在尾部操做,所以 path 变量是一个栈;
  • 深度优先遍历经过「回溯」操做,实现了全局使用一份状态变量的效果。

使用编程的方法获得全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点造成的路径就是其中一个全排列。因此,树的最后一层就是递归终止条件。

代码实现:

var permute = function(nums) {
  let len = nums.length, res= [];
  if(!len) return res;

  let used = []; // boolean[]
  let path = []; //number[]
  dfs(nums, len, 0, path, used, res);
  return res;

  function dfs(nums, len, depth, path, used, res){
      if(depth === len) {
          //path是动态数组,不能直接push,须要拷贝一份当前值保存到结果中
          res.push([...path]); 
          return;
      }
      
      // 针对全排列中下标为depth的位置进行全部可能的尝试
      for(let i=0; i<len; i++){
          if(!used[i]){
              path.push(nums[i]);
              used[i] = true;

              // 往下找全排列中的下一个位置
              dfs(nums, len, depth+1, path, used, res);

              // 造成一个全排列后,进行回退,尝试其余答案
              used[i] = false;
              path.pop();
          }
      }
  }
};
复制代码
  • 首先这棵树除了根结点和叶子结点之外,每个结点作的事情实际上是同样的,即:在已经选择了一些数的前提下,在剩下的尚未选择的数中,依次选择一个数,这显然是一个 递归 结构;
  • 递归的终止条件是: 一个排列中的数字已经选够了 ,所以咱们须要一个变量来表示当前程序递归到第几层,咱们把这个变量叫作 depth,或者命名为 index ,表示当前要肯定的是某个全排列中下标为 index 的那个数是多少;
  • 布尔数组 used,初始化的时候都为 false 表示这些数尚未被选择,当咱们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就可以以 O(1) 的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。

这些变量称为「状态变量」,它们表示了在求解一个问题的时候所处的阶段。须要根据问题的场景设计合适的状态变量。

N皇后

leetcode 51 如何将 n 个皇后放置在 n×n 的棋盘上,而且使皇后彼此之间不能相互攻击

输入:4
输出:[ [".Q..", Q","Q...","..Q."], ["..Q.","Q...", "...Q", ".Q.."] ]

  • 遍历每一行的每一列,若是当前位置不会产生攻击就记录当前位置并结束本行循环,往下一行走;若是本行没有一个位置能安放,或者已经走完全部行,就回退到上一个安放的位置,继续看此处的下一个位置可否安放,往复循环。
  • 那么,重点是怎么判断当前位置可否安放,在循环中,一行只能放一个,放下以后就立马进入下一行,因此一行中不会有重复的项,那列和对角线呢?咱们使用三个数组来分别记录列和主副对角线的使用状况,当某个位置放下一个皇后以后,记录该列到列数组中,此后该列不能使用;
  • 关于对角线:

主对角线规律:x-y=k(行-列=固定值)
副对角线规律:x+y=k(行+列=固定值)
因此,当某个位置放下一个皇后以后,记录当前行+列的值,和行-列的值,此后的位置若是行+列或行-列有与数组中重复的,都不可以使用。

代码实现:

var solveNQueens = function(n) {
  if(n==0) return res;

  let col = [], main = [], sub = []; // boolean[]
  let res = []; // string[]
  let path = []; //number[]
  dfs(0, path);
  return res;

  function dfs(row, path){
      // 深度优先遍历到下标为 n,表示 [0.. n - 1] 已经填完,获得了一个结果
      if(row == n){
          const board = convert2board(path);
          res.push(board);
          return;
      }

      // 针对下标为 row 的每一列,尝试是否能够放置
      for(let j=0; j<n; j++){
          if(!col[j] && !main[row-j+n-1] && !sub[row+j]){
              path.push(j);

              // 记录该位置的攻击范围
              col[j] = true;
              main[row-j+n-1] = true; //加n-1是为了防止数组索引为负数
              sub[row+j] = true;

              // 进入下一行
              dfs(row+1, path);

              // 回溯, 去掉path中最后一个值,尝试其余选项
              col[j] = false;
              main[row-j+n-1] = false; 
              sub[row+j] = false;
              path.pop();
          }
      }
  }

  // 输出一个结果
  function convert2board(path){
      let board = []; // string[]
      for(let i=0; i<path.length; i++){
          let ret = new Array(n).fill('.');
          ret[path[i]] = 'Q';
          board.push(ret.join(''))
      }
      return board;
  }
};
复制代码

回溯算法解题模板

经过以上几个问题不难发现,回溯算法解题的要点就是要定义好状态变量,不断对状态进行推动、回退,尝试全部可能的解,并在状态处于递归树的叶子节点时输出此时的方案。

// 简单模版
function backtrack(){
  let res = [];
  let used = [];

  function dfs(depth, path){ // depth表示当前所在的阶段
    // 递归终止条件
    if(depth === len){
      res.push(path);
      return;
    }

    // 针对当前depth尝试全部可能的结果
    for(let i=0; i<len; i++){
      if(!used[i]){ // 此路不通的标记
        path.push(nums[i]);
        used[i] = true;

        // depth+1 前往下一个阶段
        dfs(depth+1, path);

        // 重置本阶段状态,尝试本阶段的其余可能
        used[i] = false;
        path.pop();
      }
    }
  }
}
复制代码
相关文章
相关标签/搜索