回溯法套路總結與應用

概述

回溯法常用於遍歷一個列表元素的所有所有子集,比如全排列問題。可以說深度優先搜索就是回溯法的一種特殊形式。該方法的時間複雜度比較大一般爲O(N!),它不像動態規劃存在重疊子問題可以優化,當然在某些情況下,我們可以通過剪枝來進行優化,當然這個技巧性可能較高。本篇文章主要從回溯法的基本思想入手,總結出一套模板,靈活運用模板便可以講解大部分回溯算法的問題。

模板與算法

回溯法其實核心思想就是以深度優先進行暴力窮舉,最重要的是做到補充不漏,整個過程跟決策樹的遍歷是類似的。其通用的一個算法模板如下:

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

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

從這個模板中,我們可以看出,要用好回溯算法其實重點就是解決三個問題:

  1. 路徑:也就是已經做出的選擇
  2. 選擇列表
  3. 結束條件

下邊我們通過一個全排列問題來展開說明:

全排列問題

具體題目如下:

image-20201117220437744

問題本身很簡單,我們高中的時候可能就會通過畫決策樹來解決整個問題:

爲啥說這是決策樹呢,因爲你在每個節點上其實都在做決策。比如說你站在下圖的紅色節點上:

你現在就在做決策,可以選擇 1 那條樹枝,也可以選擇 3 那條樹枝。爲啥只能在 1 和 3 之中選擇呢?因爲 2 這個樹枝在你身後,這個選擇你之前做過了,而全排列是不允許重複使用數字的。

通過前邊的說明,針對該問題需要解決的三個問題分別爲:

  1. 路徑:[2]就是路徑,記錄已經做過的選擇
  2. 選擇列表:[1,3]就是選擇列表,表示接下來可進行選擇元素的列表
  3. 結束條件:便利到樹的底層,也就是說選擇列表爲空的時候結束

進而套用上邊的模板,我們可以得出如下代碼:

  List<List<Integer>> result = new ArrayList<>();

    /**
     * 使用回溯法,解決全排列問題
     * @param nums
     * @return
     */
    public List<List<Integer>> permute(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        backtrace(nums, track);
        return result;
    }

    private void backtrace(int[] nums, LinkedList<Integer> track) {
        // add track
        if (track.size() == nums.length) {
            result.add(new LinkedList<>(track));
        }
        for (int num : nums) {
            if (track.contains(num)) {
                continue;
            }
            // make choice
            track.add(num);
            // recursive
            backtrace(nums, track);
            // backtrace choince
            track.removeLast();
        }
    }

應用

同樣的再做一道題目練習一下:

image-20201117221202350

該問題,較之上一個問題,最大的不同就是會有重複的元素,因此會增加重複元素的判斷,但整體難度也不大,套用模板可以得出如下解決方案:

List<List<Integer>> result = new ArrayList<>();
  byte[] visited = new byte[22];

  public List<List<Integer>> permuteUnique(int[] nums) {
    LinkedList<Integer> track = new LinkedList<>();
    Arrays.sort(nums);
    backtrace(nums, track);
    return result;
  }

  private void backtrace(int[] nums, LinkedList<Integer> track) {
    // 倡導到達nums.lenghth則記錄路徑
    if (nums.length == track.size()) {
      result.add(new LinkedList<>(track));
    }
    for (int i = 0; i < nums.length; i++) {
      //已經添加過的元素直接跳過
      if (visited[i] == 1) {
        continue;
      }
      //與上一個元素相同,且沒有添加過直接跳過
      if (i != 0 && nums[i] == nums[i - 1] && visited[i - 1] == 0) {
        continue;
      }
      visited[i] = 1;
      track.add(nums[i]);
      backtrace(nums, track);
      track.removeLast();
      visited[i] = 0;
    }
  }

總結

回溯算法就是個多叉樹的遍歷問題,關鍵就是在前序遍歷和後序遍歷的位置做一些操作,算法框架如下:

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

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

寫 backtrack 函數時,需要維護走過的「路徑」和當前可以做的「選擇列表」,當觸發「結束條件」時,將「路徑」記入結果集。

只要確定了選擇的條件,以及記錄路徑的調整,整個代碼很容易便可以實現出來了。

參考

  1. https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/hui-su-suan-fa-xiang-jie-xiu-ding-ban#san-zui-hou-zong-jie
  2. https://greyireland.gitbook.io/algorithm-pattern/suan-fa-si-wei/backtrack#permutations
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章