回溯演算法

zhhfan發表於2021-03-26

回溯演算法

什麼是回溯演算法

回溯演算法本質就是列舉,在給定的列舉集合中不斷從其中嘗試搜尋找到問題的解,如果在搜尋過程中發現不滿足求解條件,則回溯返回,嘗試其他路徑繼續搜尋解決,這種走不通就回退再嘗試其他路徑的方法就是回溯法。

回溯演算法解題通用套路

解決一個回溯問題,實際上就是一個決策樹的遍歷過程。你只需要思考3個問題:

  1. 路徑:也就是已經做出的選擇。
  2. 選擇列表:也就是你當前可以做的選擇。
  3. 結束條件:也就是到達決策樹底層,無法再做選擇的條件。

通用解決方案虛擬碼

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

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

它一般是解決樹形問題的,問題分解成多個階段,每個階段有多個解,這個就構成了一顆樹,所以判斷問題是否可以用回溯演算法的關鍵在於它是否可以轉成一個樹形問題。
另外我們也發現如果能夠縮小每個階段的可選解,就能讓問題的搜尋規模都縮小,這種叫做剪枝,通過剪枝能有效降低整個問題的搜尋複雜度。

回溯演算法解決全排列問題

我們在高中的時候就做過排列組合的數學題,我們也知道 n 個不重複的數,全排列共有 n! 個。
那麼我們當時是怎麼窮舉全排列的呢?比方說給三個數 [1,2,3],你肯定不會無規律地亂窮舉,一般是這樣:

先固定第一位為 1,然後第二位可以是 2,那麼第三位只能是 3;然後可以把第二位變成 3,第三位就只能是 2 了;然後就只能變化第一位,變成 2,然後再窮舉後兩位……

其實這就是回溯演算法,我們高中無師自通就會用,或者有的同學直接畫出如下這棵回溯樹:
全排列

public class Test {

    public static void main(String[] args) {
        Test test = new Test();
        int[] nums = {1, 2, 3};
        System.out.println(test.permute(nums));
    }

    public List<List<Integer>> permute(int[] nums) {
        if (nums == null || nums.length == 0) {
            return Collections.emptyList();
        }

        List<List<Integer>> result = new ArrayList<>();
        backtrack(result, new ArrayList<>(), nums);

        return result;
    }

    private void backtrack(List<List<Integer>> result, List<Integer> selectNums, int[] allNums) {
        if (selectNums.size() == allNums.length) {
            result.add(new ArrayList<>(selectNums));
            return;
        }

        for (Integer num : allNums) {
            // 剪枝
            if (selectNums.contains(num)) {
                continue;
            }

            selectNums.add(num);
            backtrack(result, selectNums, allNums);
            selectNums.remove(num);
        }
    }

}

參考資料

相關文章