回溯算法本質就是枚舉,在給定的枚舉集合中不斷從其中嘗試搜索找到問題的解,若是在搜索過程當中發現不知足求解條件,則回溯返回,嘗試其餘路徑繼續搜索解決,這種走不通就回退再嘗試其餘路徑的方法就是回溯法。算法
解決一個回溯問題,實際上就是一個決策樹的遍歷過程。你只須要思考3個問題:code
通用解決方案僞代碼blog
result = [] function backtrack(路徑, 選擇列表) { if 知足結束條件: result.add(路徑) return for 選擇 in 選擇列表: 作選擇 backtrack(路徑, 選擇列表) 撤銷選擇 }
它通常是解決樹形問題的,問題分解成多個階段,每一個階段有多個解,這個就構成了一顆樹,因此判斷問題是否能夠用回溯算法的關鍵在於它是否能夠轉成一個樹形問題。
另外咱們也發現若是可以縮小每一個階段的可選解,就能讓問題的搜索規模都縮小,這種叫作剪枝,經過剪枝能有效下降整個問題的搜索複雜度。rem
咱們在高中的時候就作過排列組合的數學題,咱們也知道 n 個不重複的數,全排列共有 n! 個。
那麼咱們當時是怎麼窮舉全排列的呢?比方說給三個數 [1,2,3],你確定不會無規律地亂窮舉,通常是這樣:get
先固定第一位爲 1,而後第二位能夠是 2,那麼第三位只能是 3;而後能夠把第二位變成 3,第三位就只能是 2 了;而後就只能變化第一位,變成 2,而後再窮舉後兩位……數學
其實這就是回溯算法,咱們高中無師自通就會用,或者有的同窗直接畫出以下這棵回溯樹:
io
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); } } }
參考資料function