概述
回溯法常用於遍歷一個列表元素的所有所有子集,比如全排列問題。可以說深度優先搜索就是回溯法的一種特殊形式。該方法的時間複雜度比較大一般爲O(N!),它不像動態規劃存在重疊子問題可以優化,當然在某些情況下,我們可以通過剪枝來進行優化,當然這個技巧性可能較高。本篇文章主要從回溯法的基本思想入手,總結出一套模板,靈活運用模板便可以講解大部分回溯算法的問題。
模板與算法
回溯法
其實核心思想就是以深度優先進行暴力窮舉,最重要的是做到補充不漏,整個過程跟決策樹
的遍歷是類似的。其通用的一個算法模板如下:
result = []
def backtrack(路徑, 選擇列表):
if 滿足結束條件:
result.add(路徑)
return
for 選擇 in 選擇列表:
做選擇
backtrack(路徑, 選擇列表)
撤銷選擇
從這個模板中,我們可以看出,要用好回溯算法其實重點就是解決三個問題:
- 路徑:也就是已經做出的選擇
- 選擇列表
- 結束條件
下邊我們通過一個全排列問題來展開說明:
全排列問題
具體題目如下:
問題本身很簡單,我們高中的時候可能就會通過畫決策樹來解決整個問題:
爲啥說這是決策樹呢,因爲你在每個節點上其實都在做決策。比如說你站在下圖的紅色節點上:
你現在就在做決策,可以選擇 1 那條樹枝,也可以選擇 3 那條樹枝。爲啥只能在 1 和 3 之中選擇呢?因爲 2 這個樹枝在你身後,這個選擇你之前做過了,而全排列是不允許重複使用數字的。
通過前邊的說明,針對該問題需要解決的三個問題分別爲:
- 路徑:[2]就是路徑,記錄已經做過的選擇
- 選擇列表:[1,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();
}
}
應用
同樣的再做一道題目練習一下:
該問題,較之上一個問題,最大的不同就是會有重複的元素,因此會增加重複元素的判斷,但整體難度也不大,套用模板可以得出如下解決方案:
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 函數時,需要維護走過的「路徑」和當前可以做的「選擇列表」,當觸發「結束條件」時,將「路徑」記入結果集。
只要確定了選擇的條件,以及記錄路徑的調整,整個代碼很容易便可以實現出來了。