一文學會回溯算法解題技巧中對回溯法的描述很通俗易懂,現將基本概念遷移到此。
深度優先算法用到了回溯的算法思想,這個算法雖然相對比較簡單,但很重要,在生產上廣泛用在正則表達式,編譯原理的語法分析等地方,很多經典的面試題也可以用回溯算法來解決,如八皇后問題,排列組合問題,0-1背包問題,數獨問題等,也是一種非常重要的算法。
什么是回溯算法
回溯算法本質其實就是枚舉,在給定的枚舉集合中,不斷從其中嘗試搜索找到問題的解,如果在搜索過程中發現不滿足求解條件 ,則「回溯」返回,嘗試其它路徑繼續搜索解決,這種走不通就回退再嘗試其它路徑的方法就是回溯法,許多復雜的,規模較大的問題都可以使用回溯法,所以回溯法有「通用解題方法」的美稱。
回溯算法解題通用套路
為了有規律地求解問題,我們把問題分成多個階段,每個階段都有多個解,隨機選擇一個解,進入下一個階段,下一個階段也隨機選擇一個解,再進入下一個階段...
每個階段選中的解都放入一個 「已選解集合」 中,並且要判斷 「已選解集合」是否滿足問題的條件(base case),有兩種情況
- 如果「已選解集合」滿足問題的條件,則將 「已選解集合」放入「結果集」中,並且「回溯」換個解再遍歷。
- 如果不滿足,則「回溯」換個解再遍歷
根據以上描述不難得出回溯算法的通用解決套路偽代碼如下:
function backtrace(已選解集合,每個階段可選解) { if (已選解集合滿足條件) { 結果集.add(已選解集合); return; } // 遍歷每個階段的可選解集合 for (可選解 in 每個階段的可選解) { // 選擇此階段其中一個解,將其加入到已選解集合中 已選解集合.add(可選解) // 進入下一個階段 backtrace(已選解集合,下個階段可選的空間解) // 「回溯」換個解再遍歷 已選解集合.remove(可選解) } }
通過以上分析我們不難發現回溯算法本質上就是深度優先遍歷,它一般解決的是樹形問題(問題分解成多個階段,每個階段有多個解,這樣就構成了一顆樹),所以判斷問題是否可以用回溯算法的關鍵在於它是否可以轉成一個樹形問題。
另外我們也發現如果能縮小每個階段的可選解,就能讓問題的搜索規模都縮小,這種就叫「剪枝」,通過剪枝能有效地降低整個問題的搜索復雜度!
綜上,我們可以得出回溯算法的基本套路如下:
- 將問題分成多個階段,每個階段都有多個不同的解,這樣就將問題轉化成了樹形問題,這一步是問題的關鍵!如果能將問題轉成樹形問題,其實就成功了一半,需要注意的是樹形問題要明確終止條件,這樣可以在 DFS 的過程中及時終止遍歷,達到剪枝的效果
- 套用上述回溯算法的解題模板,進行深度優先遍歷,直到找到問題的解。
回溯算法實現三數之和
public class ThreeNumSum1 { public static void main(String[] args) { // TODO Auto-generated method stub ThreeNumSum1 t = new ThreeNumSum1(); int[] nums= {5,-11,-7,-2,4,9,4,4,-5,12,12,-14,-5,3,-3,-2,-6,3,3,-9}; t.threeSum(nums); for(List<Integer> l : t.res) { System.out.print(l.get(0) + "\t"); System.out.print(l.get(1) + "\t"); System.out.print(l.get(2)); System.out.println(); } } List<List<Integer>> res = new ArrayList<>(); List<Integer> selected = new ArrayList<>();//記錄索引值 public List<List<Integer>> threeSum(int[] nums) { backtrace(nums); return res; } private void backtrace(int[] nums){ if(selected.size() == 3){ if((nums[selected.get(0)] + nums[selected.get(1)] + nums[selected.get(2)]) == 0) { List<Integer> tmp = new ArrayList<>(); tmp.add(nums[selected.get(0)]); tmp.add(nums[selected.get(1)]); tmp.add(nums[selected.get(2)]); Collections.sort(tmp); if(!res.contains(tmp)){ res.add(tmp); } } return; } for(int i = 0; i < nums.length; i++){ if(selected.contains(i)) continue; selected.add(i); backtrace(nums); selected.remove(selected.size()-1); } } }
輸出:
-3 -2 5 -9 4 5 -14 5 9 -7 -2 9 -7 3 4 -7 -5 12 -2 -2 4 -6 -3 9 -9 -3 12 -6 3 3