簡單易懂的回溯算法(Back Tracking)

回溯法(Back Tracking Method)(探索與回溯法)是一種選優搜索法,又稱爲試探法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步從新選擇,這種走不通就退回再走的技術爲回溯法,而知足回溯條件的某個狀態的點稱爲「回溯點」。web

能夠把回溯法當作是遞歸調用的一種特殊形式。算法

代碼方面,回溯算法的框架:微信

result = []
def backtrack(路徑, 選擇列表):
if 知足結束條件:
result.add(路徑)
return

for 選擇 in 選擇列表:
作選擇
backtrack(路徑, 選擇列表)
撤銷選擇

其核心就是 for 循環裏面的遞歸,在遞歸調用以前「作選擇」,在遞歸調用以後「撤銷選擇」,特別簡單。app

總結就是:框架

循環 + 遞歸 = 回溯

引言

回溯算法實際上一個相似枚舉的搜索嘗試過程,主要是在搜索嘗試過程當中尋找問題的解,當發現已不知足求解條件時,就「回溯」返回,嘗試別的路徑。編輯器

回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步從新選擇,這種走不通就退回再走的技術爲回溯法,而知足回溯條件的某個狀態的點稱爲「回溯點」。函數

許多複雜的,規模較大的問題均可以使用回溯法,有「通用解題方法」的美稱。flex

算法思想

回溯(backtracking) 是一種系統地搜索問題解答的方法。爲了實現回溯,首先須要爲問題定義一個解空間(solution space),這個空間必須至少包含問題的一個解(多是最優的)。下一步是組織解空間以便它能被容易地搜索。典型的組織方法是圖(迷宮問題)或樹(N皇后問題)。一旦定義瞭解空間的組織方法,這個空間便可按深度優先的方法從開始節點進行搜索。spa

回溯方法的步驟以下:.net

  • 定義一個解空間,它包含問題的解。
  • 用適於搜索的方式組織該空間。
  • 用深度優先法搜索該空間,利用限界函數避免移動到不可能產生解的子空間。

回溯算法的一個有趣的特性是在搜索執行的同時產生解空間。在搜索期間的任什麼時候刻,僅保留從開始節點到當前節點的路徑。所以,回溯算法的空間需求爲O(從開始節點起最長路徑的長度)。這個特性很是重要,由於解空間的大小一般是最長路徑長度的指數或階乘。因此若是要存儲所有解空間的話,再多的空間也不夠用。

算法應用

回溯算法的求解過程實質上是一個先序遍歷一棵"狀態樹"的過程,只是這棵樹不是遍歷前預先創建的,而是隱含在遍歷過程當中.

  • 冪集問題(組合問題) 求含N個元素的集合的冪集。
如對於集合A={1,2,3},則A的冪集爲
p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},Φ}

冪集的每一個元素是一個集合,它或是空集,或含集合A中的一個元素,或含A中的兩個元素,或者等於集合A。反之,集合A中的每個元素,它只有兩種狀態:屬於冪集的元素集,或不屬於冪集元素集。則求冪集P(A)的元素的過程可當作是依次對集合A中元素進行「取」或「舍」的過程,而且能夠用一棵狀態樹來表示。求冪集元素的過程即爲先序遍歷這棵狀態樹的過程。

遞歸和迭代回溯

通常狀況下能夠用遞歸函數實現回溯法,遞歸函數模板以下:

void BackTrace(int t) {
    if(t>n)
        Output(x);
    else
        for(int i = f (n, t); i <= g (n, t); i++ ) {
            x[t] = h(i);
            if(Constraint(t) && Bound (t))
                BackTrace(t+1);
        }
}

其中,t 表示遞歸深度,即當前擴展結點在解空間樹中的深度;n 用來控制遞歸深度,即解空間樹的高度。當 t>n時,算法已搜索到一個葉子結點,此時由函數Output(x)對獲得的可行解x進行記錄或輸出處理

用 f(n, t)和 g(n, t)分別表示在當前擴展結點處未搜索過的子樹的起始編號和終止編號;h(i)表示在當前擴展結點處x[t] 的第i個可選值;函數 Constraint(t)和 Bound(t)分別表示當前擴展結點處的約束函數和限界函數。

若函數 Constraint(t)的返回值爲真,則表示當前擴展結點處x[1:t] 的取值知足問題的約束條件;不然不知足問題的約束條件。若函數Bound(t)的返回值爲真,則表示在當前擴展結點處x[1:t] 的取值還沒有使目標函數越界,還需由BackTrace(t+1)對其相應的子樹作進一步地搜索;不然,在當前擴展結點處x[1:t]的取值已使目標函數越界,可剪去相應的子樹。

採用迭代的方式也可實現回溯算法,迭代回溯算法的模板以下:

void IterativeBackTrace(void) {
    int t = 1;
    while(t>0) {
        if(f(n, t) <= g( n, t))
            for(int i = f(n, t); i <= g(n, t); i++ ) {
                x[t] = h(i);
                if(Constraint(t) && Bound(t)) {
                    if ( Solution(t))
                        Output(x);
                    else
                        t++;
                }
            }
            else t−−;
    }
}

在上述迭代算法中,用Solution(t)判斷在當前擴展結點處是否已獲得問題的一個可行解,若其返回值爲真,則表示在當前擴展結點處x[1:t] 是問題的一個可行解;不然表示在當前擴展結點處x[1:t]只是問題的一個部分解,還須要向縱深方向繼續搜索。用回溯法解題的一個顯著特徵是問題的解空間是在搜索過程當中動態生成的,在任什麼時候刻算法只保存從根結點到當前擴展結點的路徑。若是在解空間樹中,從根結點到葉子結點的最長路徑長度爲 h(n),則回溯法所需的計算空間複雜度爲 O(h(n)),而顯式地存儲整個解空間複雜度則須要O(2h(n))或O(h(n)!)。

全排列

  • 循環+遞歸
function DFS(nums = []{
      let res = [];
      const dfs = (path = []) => {
          if (path.length == nums.length) {
              res.push([...path]);
              return;
          }

          for (let i = 0; i < nums.length; i++) {
              if (path.includes(nums[i])) {
                  continue;
              }

              path.push(nums[i]);
              dfs(path)
              path.pop();
          }
      }
      dfs([]);
      return res;
  }

  console.log(DFS([123]));

  console.log(DFS([1234]));
  • 交換法
function permuts(nums = []{
    let res = [];
    const swap = (p, q) => {
        if (p == q) return;
        [nums[p], nums[q]] = [nums[q], nums[p]];
    }

    const dfs = (p, q) => {
        if (p == q) {
            res.push([...nums]);
            return;
        }

        for (let i = p; i <= q; i++) {
            swap(p, i);
            dfs(p + 1, q);
            swap(p, i);
        }
    }
    dfs(0, nums.length - 1);

    return res;
}

console.log(permuts([123]));


本文分享自微信公衆號 - JavaScript忍者祕籍(js-obok)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索