回溯算法總結(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();
      }
    }
  }
}
複製代碼
相關文章
相關標籤/搜索