這一篇文章來說解一下如何作leetcode回溯算法題目,這一段時間我把leetcode上面的回溯算法的題目都刷了個遍,發現了其中一些規律,因此,就想寫一篇文章來總結一下,怕之後忘記。java
刷完回溯算法的題目,我發現其實能夠總結爲三大類:子集問題、組合問題、排列問題,那這三大類都是什麼意思呢,我分別舉一個例子來講明。算法
子集問題,好比說,數組[1,2,3]
,那麼對應的子集問題就是,這個數組的子集有:[],[1],[2],[3],[1,3],[2,3],[1,2],[1,2,3]
,這就是這個數組的子集,這一類問題在leetcode上面不少個,並且有些題目數組中的元素是能夠重複的,而後來求子集問題。數組
組合問題,好比說,數組[1,2,3]
,組合出target爲3的可能的選擇,那麼就有:[1,2],[3]
,這就是leetcode中的組合問題。框架
排列問題,排列問題就比較簡單了,好比,咱們常見的全排列問題,leetcode也有這一類問題。函數
這篇文章,咱們就來說講,怎麼用回溯的算法去解決這些問題。spa
最開始,我仍是想經過一個簡單的例子,一步一步的帶你們看一下回溯算法的題目應該是怎麼一步一步解決的,最終,經過這個題目,咱們就能夠大體的整理出一個回溯算法的解題框架;先來看下面這個題目,是一個子集的題目,題目難度中等。code
這個題目,題目給的框架是這樣的。blog
public List<List<Integer>> subsets(int[] nums) { }
因此,咱們就知道,咱們先構建一個List<List<Integer>>
類型的返回值。排序
List<List<Integer>> list = new ArrayList<>();
接下來,咱們就開始寫回溯方法。遞歸
public void backTrace(int start, int[] nums, List<Integer> temp){ for(int j = 0; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
最開始,可能寫成上面這個樣子,傳入數組nums
,start
和temp集合
用於保存結果,而後,每次遍歷數組nums的時候,都加入當前元素,在遞歸回來的時候再回溯,刪除剛剛加入的元素,這不就是回溯的思想嗎。
這樣把基本的框架寫完了,還有一個須要思考的問題就是base case,那麼這個題目的base case是什麼呢?其實,由於是子集,每一步都是須要加入到結果集合temp的,因此就沒有什麼限制條件了。
public void backTrace(int start, int[] nums, List<Integer> temp){ //每次都保存結果 list.add(new ArrayList<>(temp)); for(int j = 0; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
最後,咱們再補充完整一下,就完整的代碼出來了。
List<List<Integer>> list = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { if(nums.length == 0){ return null; } List<Integer> temp = new ArrayList<>(); backTrace(0, nums, temp); return list; } public void backTrace(int start, int[] nums, List<Integer> temp){ list.add(new ArrayList<>(temp)); for(int j = 0; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
ok,咱們去運行一下,看看如何。
他說我超出時間限制,說明算法是有問題的,咱們再看一下上面咱們寫的代碼,咱們發現,其實咱們每次遍歷數組的時候都是從0開始遍歷的,致使不少重複的元素遍歷了,也就是咱們得start
變量並無用到,最後,咱們把遍歷的時候不每次從0開始,而是從當前的start開始遍歷,選過的元素咱們排除,看一下結果。
List<List<Integer>> list = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { if(nums.length == 0){ return null; } List<Integer> temp = new ArrayList<>(); backTrace(0, nums, temp); return list; } public void backTrace(int start, int[] nums, List<Integer> temp){ list.add(new ArrayList<>(temp)); //從start開始遍歷,避免重複 for(int j = start; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
發現完美經過,good job!!
另外,咱們要注意一個點就是:list.add(new ArrayList<>(temp))
;不要寫成list.add(temp);
,不然,輸出的結果就是空集,你思考一下應該就知道爲何了。
經過,這個題目,其實,咱們就把回溯算法的一個大體的框架能夠整理出來了,之後作其餘題目,照貓畫虎,一頓操做就能夠了。
回到backTrace函數,其實就是一個選擇/撤銷選擇的過程,其中的for循環也是一個選擇的過程,還有一個點就是base case須要在這個函數來處理。那麼,咱們就能夠把框架整理出來。
public void backTrace(int start, int[] nums, List<Integer> temp){ base case處理 //選擇過程 for(循環選擇){ 選擇 backTrace(遞歸); 撤銷選擇 } }
ok,上面已經講了一個子集的問題,接下來,再來一個更有點意思的子集的題目。
用於引入回溯算法框架的那個題目其實比較簡單,可是,思想是不變的,這個框架很重要,其餘的題目基本上都是在上面的框架上進行修改的,好比,剪枝操做等。
這個題目與前面的子集題目相比較,差異就在於補鞥呢包含重複的子集,也就是不能順序改變而已,元素同樣的子集出現。
這個題目框架仍是不變的,可是,要作一下簡單的剪枝操做:怎麼排除掉重複的子集。
這裏有兩種方法能夠解決這個問題,並且,後面其餘的題目出現不能出現重複子集這樣的限制條件的時候,都是能夠用這兩種方法進行解決的。
咱們仍是先把上面的框架搬下來,而後再進行修改。
List<List<Integer>> list = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { if(nums.length == 0){ return null; } List<Integer> temp = new ArrayList<>(); backTrace(0, nums, temp); return list; } public void backTrace(int start, int[] nums, List<Integer> temp){ list.add(new ArrayList<>(temp)); //從start開始遍歷,避免重複 for(int j = start; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
由於咱們要利用Set的特性去重,因此須要加入這個變量Set<List<Integer>> set = new HashSet<>();
,另外,爲了保證順序,咱們再進行排序Arrays.sort(nums)
,這樣能避免元素同樣,可是順序不同的重複子集問題。
因此,結果就出來了。
List<List<Integer>> list = new ArrayList<>(); Set<List<Integer>> set = new HashSet<>(); public List<List<Integer>> subsetsWithDup(int[] nums) { if(nums.length == 0){ return null; } //排序 Arrays.sort(nums); List<Integer> temp = new ArrayList<>(); backTrace(0, nums, temp); return list; } public void backTrace(int start, int[] nums, List<Integer> temp){ //set去重操做 if(!set.contains(temp)){ set.add(new ArrayList<>(temp)); list.add(new ArrayList<>(temp)); } for(int j = start; j < nums.length; j++){ temp.add(nums[j]); backTrace(j+1,nums,temp); temp.remove(temp.size()-1); } }
看一下結果發現效率不是很好。
那咱們再來看一下另一種剪枝的策略用來去重。
i > start && nums[i-1] == nums[i]
這種剪枝策略爲何是能夠的呢,別急,我來畫張圖解釋一下。
因此,咱們這種方法就能夠作出來了。
List<List<Integer>> list = new ArrayList<>(); public List<List<Integer>> subsetsWithDup(int[] nums) { if(nums.length == 0){ return null; } Arrays.sort(nums); List<Integer> temp = new ArrayList<>(); backTrace(0, nums, temp); return list; } public void backTrace(int start, int[] nums, List<Integer> temp){ list.add(new ArrayList<>(temp)); for(int i = start; i < nums.length; i++){ //剪枝策略 if(i > start && nums[i] == nums[i-1]){ continue; } temp.add(nums[i]); backTrace(i+1,nums,temp); temp.remove(temp.size()-1); } }
哎呦,好像還能夠哦。
把前面的子集問題搞定以後,你會發現,後面的組合問題,排列問題就都不是什麼大問題了,基本上都是套路了。
這個題目跟以前的沒有什麼太大的區別,只是須要注意一個點:每一個數字能夠被無限制重複被選取,咱們要作的就是在遞歸的時候,i
的下標不是從i+1
開始,而是從i
開始。
backTrace(i,candidates,target-candidates[i], temp);
咱們看看完整代碼。
List<List<Integer>> list = new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { if(candidates.length == 0 || target < 0){ return list; } List<Integer> temp = new ArrayList<>(); backTrace(0,candidates,target,temp); return list; } public void backTrace(int start, int[] candidates, int target, List<Integer> temp){ //遞歸的終止條件 if (target < 0) { return; } if(target == 0){ list.add(new ArrayList<>(temp)); } for(int i = start; i < candidates.length; i++){ temp.add(candidates[i]); backTrace(i,candidates,target-candidates[i], temp); temp.remove(temp.size()-1); } }
就是這麼簡單!!!
那麼,再來一個組合問題。
你一看題目是否是就發現,差很少啊,確實,這裏只是每一個數字只能用一次,同時也是不能包含重複的組合,因此,用上面的去重方法解決咯。話很少說,上代碼。
List<List<Integer>> lists = new LinkedList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) { if(candidates.length == 0 || target < 0){ return lists; } Arrays.sort(candidates); List<Integer> list = new LinkedList<>(); backTrace(candidates,target,list, 0); return lists; } public void backTrace(int[] candidates, int target, List<Integer> list, int start){ if(target == 0){ lists.add(new ArrayList(list)); } for(int i = start; i < candidates.length; i++){ if(target < 0){ break; } //剪枝:保證同一層中只有1個相同的元素,不一樣層能夠有重複元素 if(i > start && candidates[i] == candidates[i-1]){ continue; } list.add(candidates[i]); backTrace(candidates,target-candidates[i],list,i+1); list.remove(list.size()-1); } }
也是完美解決!!
先來一個最基本的全排列問題,快速解決。
這是全排列,只是元素的順序不同,因此,咱們要作的剪枝就是:temp集合中有的就排除。
上代碼。
List<List<Integer>> lists = new ArrayList<>(); public List<List<Integer>> permute(int[] nums) { if(nums.length == 0){ return lists; } List<Integer> list = new ArrayList<>(); backTrace(nums,list,0); return lists; } public void backTrace(int[] nums, List<Integer> temp, int start){ if(temp.size() == nums.length){ lists.add(new ArrayList(temp)); return; } for(int i = 0; i < nums.length; i++){ //排除已有元素 if(temp.contains(nums[i])){ continue; } temp.add(nums[i]); backTrace(nums,temp,i+1); temp.remove(temp.size() - 1); } }
是否是不帶勁,安排!!
這個題目雖然也是全排列,可是,就要比前面這個難一些了,有兩個限定條件:有重複元素,可是不能包含重複排列。
不重複的全排列這個咱們知道怎麼解決,用前面的去重方法便可,可是,怎麼保證有相同元素的集合不出現重複的排列呢?
這裏咱們須要加一個visited數組,來記錄一下當前元素有沒有被訪問過,這樣就能夠解題了。
public List<List<Integer>> result = new ArrayList<>(); public List<List<Integer>> permuteUnique(int[] nums) { if(nums.length == 0){ return result; } Arrays.sort(nums); findUnique(nums,new boolean[nums.length],new LinkedList<Integer>()); return result; } public void findUnique(int[] nums, boolean[] visited,List<Integer> temp){ //結束條件 if(temp.size() == nums.length){ result.add(new ArrayList<>(temp)); return ; } //選擇列表 for(int i = 0; i<nums.length; i++){ //已經選擇過的不須要再放進去了 if(visited[i]) continue; //去重 if(i>0 && nums[i] == nums[i-1] && visited[i-1]) break; temp.add(nums[i]); visited[i] = true; findUnique(nums,visited,temp); temp.remove(temp.size()-1); visited[i] = false; } }
這樣就搞定了這個題目。
至此,就把子集、組合、全排列問題給解決了。從一步一步講解框架,到具體問題分析,面面俱到,哈哈,固然,還有一些沒有考慮周到的地方,望你們指教。
這篇文章寫了兩天了,到這裏差很少了,原創不易,點個贊吧!