算法——回溯算法 (Back Tracking) (轉)


轉自:https://my.oschina.net/u/3024426/blog/4689026

 

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

可以把回溯法看成是遞歸調用的一種特殊形式。

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

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

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

其核心就是 for 循環里面的遞歸,在遞歸調用之前「做選擇」,在遞歸調用之后「撤銷選擇」,特別簡單。

總結就是:

循環 + 遞歸 = 回溯

引言

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

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

許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。

算法思想

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

回溯方法的步驟如下:

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

回溯算法的一個有趣的特性是在搜索執行的同時產生解空間。在搜索期間的任何時刻,僅保留從開始節點到當前節點的路徑。因此,回溯算法的空間需求為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([1, 2, 3]));

  console.log(DFS([1, 2, 3, 4]));
  • 交換法
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([1, 2, 3]));



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM