儲存載入模型model.save()
本文參考:程式碼隨想錄
回溯演算法能解決如下問題:
- 組合問題:N個數裡面按一定規則找出k個數的集合
- 排列問題:N個數按一定規則全排列,有幾種排列方式
- 切割問題:一個字串按一定規則有幾種切割方式
- 子集問題:一個N個數的集合裡有多少符合條件的子集
- 棋盤問題:N皇后,解數獨等等
回溯演算法的模板:
void backtracking(引數) { if (終止條件) { 存放結果; return; } for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) { 處理節點; backtracking(路徑,選擇列表);// 遞迴 回溯,撤銷處理結果 } }
型別一:組合問題
1. 給定兩個整數 n 和 k,返回 1 ... n 中所有可能的 k 個數的組合。
示例:
輸入:n = 4, k = 2
輸出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
本題這是回溯法的經典題目。
直接的解法當然是使用for迴圈,例如示例中k為2,很容易想到 用兩個for迴圈,這樣就可以輸出 和示例中一樣的結果。
int n = 4; for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n; j++) { System.out.println( i+ " " + j); } }
k=2 需要兩層迴圈,但k=50,那這。。。
使用回溯演算法:
結合上圖說下大體思想,回溯與DFS遞迴演算法結合,dfs有引數begin表示開始的位置,也就是選中的值,從1到 n 。每次遞迴執行dfs就會把新的begin儲存到 Deque<Integer> path 也就是雙向佇列之中。因為數字不能重複,第一個數字選擇1,下一次dfs就要從2開始。在本題中取完[1,2],path長度與k相等,我們的得到第一種結果[1,2]。這是我們想得到下一種情況,就需要在遞迴語句執行後(本次遞迴中已經把這種情況寫入ans中)把2吐出來,方便下次得到[1,3]這就是回溯。
public class Solution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> res = new ArrayList<>(); if (k <= 0 || n < k) { return res; } // 從 1 開始是題目的設定 Deque<Integer> path = new ArrayDeque<>(); dfs(n, k, 1, path, res); return res; } private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res) { // 遞迴終止條件是:path 的長度等於 k if (path.size() == k) { res.add(new ArrayList<>(path)); return; } // 遍歷可能的搜尋起點 for (yinweiwomenint i = begin; i <= n; i++) { // 向路徑變數裡新增一個數 path.addLast(i); // 下一輪搜尋,設定的搜尋起點要加 1,因為組合數理不允許出現重複的元素 dfs(n, k, i + 1, path, res); // 重點理解這裡:深度優先遍歷有回頭的過程,因此遞迴之前做了什麼,遞迴之後需要做相同操作的逆向操作 path.removeLast(); } } }
組合問題型別二:
給定一個無重複元素的陣列 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。
candidates 中的數字可以無限制重複被選取。
說明:
- 所有數字(包括 target)都是正整數。
- 解集不能包含重複的組合。
示例 1:
輸入:candidates = [2,3,6,7], target = 7,
所求解集為:
[
[7],
[2,2,3]
]
示例 2:
輸入:candidates = [2,3,5], target = 8,
所求解集為:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
本題沒有數量要求,元素可以重複,所以推出條件target<=0即可。同時執行下次遞迴式,從本次元素開始表示可重複。
public class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { int len = candidates.length; List<List<Integer>> res = new ArrayList<>(); if (len == 0) { return res; } Deque<Integer> path = new ArrayDeque<>(); dfs(candidates, 0, len, target, path, res); return res; } /** * @param candidates 候選陣列 * @param begin 搜尋起點 * @param len 冗餘變數,是 candidates 裡的屬性,可以不傳 * @param target 每減去一個元素,目標值變小 * @param path 從根結點到葉子結點的路徑,是一個棧 * @param res 結果集列表 */ private void dfs(int[] candidates, int begin, int len, int target, Deque<Integer> path, List<List<Integer>> res) { // target 為負數和 0 的時候不再產生新的孩子結點 if (target < 0) { return; } if (target == 0) { res.add(new ArrayList<>(path)); return; } // 重點理解這裡從 begin 開始搜尋的語意 for (int i = begin; i < len; i++) { path.addLast(candidates[i]); // 注意:由於每一個元素可以重複使用,下一輪搜尋的起點依然是 i,這裡非常容易弄錯 dfs(candidates, i, len, target - candidates[i], path, res); // 狀態重置 path.removeLast(); } } }
組合總和型別三:
給定一個數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。
candidates 中的每個數字在每個組合中只能使用一次。
說明:
所有數字(包括目標數)都是正整數。
解集不能包含重複的組合。
示例1:
輸入: candidates =[10,1,2,7,6,1,5], target =8,
所求解集為:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例2:
輸入: candidates =[2,5,2,1,2], target =5,
所求解集為:
[
[1,2,2],
[5]
]
本題不能有重複的結果,比如示例二有三個二,不能說結果裡面有三組[1,2,2],完了你告訴我說這三個二不一樣。
我們要往遞迴樹那裡考慮,先把元素排序,三個二連著,這三組[1,2,2]對應什麼樣的樹呢,
通過上圖分析,同排集合相同,最左側相同集合([1,2])的子樹會完全包含右側相同集合([1,2])的子樹,最左側[1,2]及右側子節點形成的子樹與中間[1,2]生成子樹完全一致。
所以如果遞迴到元素2這裡時,只要判斷2是不是所有2中開頭的2就可以了,不是的話直接continue;這種情況叫做“樹層去重”,會經常遇到。
public class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { List<List<Integer>> res=new ArrayList<>(); if(target<=0||candidates.length<=0)return res; Deque<Integer> path=new ArrayDeque<>(); Arrays.sort(candidates); dfs(candidates,target,0,path,res); return res; } private void dfs(int[] candidates, int target, int begin, Deque<Integer> path, List<List<Integer>> res) { if(target<=0||begin==candidates.length){ if(target==0){ res.add(new ArrayList(path)); } return; } for(int i=begin;i<candidates.length;i++){ if(i>begin&&candidates[i]==candidates[i-1]) continue; path.addLast(candidates[i]); dfs(candidates,target-candidates[i],i+1,path,res); path.removeLast(); } } }
切割問題:
給定一個字串 s,將 s 分割成一些子串,使每個子串都是迴文串。
返回 s 所有可能的分割方案。
示例:
輸入:"aab"
輸出:
[
["aa","b"],
["a","a","b"]
]
切割問題與組合問題十分類似,對比一下:
- 組合問題:選取一個a之後,在bcdef中再去選取第二個,選取b之後在cdef中在選取第三個.....。
- 切割問題:切割一個a之後,在bcdef中再去切割第二段,切割b之後在cdef中在切割第三段.....。
切割線的體現:
在處理組合問題的時候,遞迴引數需要傳入startIndex,表示下一輪遞迴遍歷的起始位置,這個startIndex就是切割線。
我們需要單獨寫個方法判斷現在切出來的是否是迴文串,就是把最前和最後比較,然後依次向內來一位。不一致就false。
public class Solution { private boolean checkPalindrome(String s,int left,int right){ while(left<right){ if(s.charAt(left)!=s.charAt(right)){ return false; } left++; right--; } return true; } public List<List<String>> partition(String s) { List<List<String>> ans=new ArrayList<>(); if(s.length()==0)return ans; Deque<String> path=new ArrayDeque<>(); dfs(s,0,path,ans); return ans; } private void dfs(String s, int begin, Deque<String> path, List<List<String>> ans) { if(begin==s.length()){ ans.add(new ArrayList<>(path)); return; } for(int i=begin;i<s.length();i++){ if(!checkPalindrome(s,begin,i)) continue; path.addLast(s.substring(begin,i+1)); dfs(s,i+1,path,ans); path.removeLast(); } } }
子集問題:
子集型別一:
給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例: 輸入: nums = [1,2,3]
輸出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
如果子集問題和切割問題是要遞迴樹的葉節點,那麼子集問題就是要所有節點(去重)
普通的去重就是再for迴圈中從startIndex開始就可以了。
public class Solution { public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> ans=new ArrayList<>(); if(nums.length==0)return ans; Deque<Integer> path=new ArrayDeque<>(); dfs(nums,0,path,ans); return ans; } private void dfs(int[] nums, int begin, Deque<Integer> path, List<List<Integer>> ans) { ans.add(new ArrayList(path)); for(int i=begin;i<nums.length;i++){ path.addLast(nums[i]); dfs(nums,i+1,path,ans); path.removeLast(); } } }
子集型別二:
給定一個可能包含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: [1,2,2]
輸出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
集合本身有重複元素,要求不同有重複子集,就是之前遇到的普通降重+樹層去重就可以滿足了,不要忘了先把結合排序。
class Solution { public List<List<Integer>> subsetsWithDup(int[] nums) { List<List<Integer>> ans=new ArrayList<>(); if(nums.length==0)return ans; Deque<Integer> path=new ArrayDeque<>(); Arrays.sort(nums); dfs(nums,0,path,ans); return ans; } private void dfs(int[] nums, int begin, Deque<Integer> path, List<List<Integer>> ans) { ans.add(new ArrayList(path)); for(int i=begin;i<nums.length;i++){ if(i>begin&&nums[i]==nums[i-1]) continue; path.addLast(nums[i]); dfs(nums,i+1,path,ans); path.removeLast(); } } }
遞增子序列:
示例:
輸入: [4, 6, 7, 7] 輸出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
說明:
- 給定陣列的長度不會超過15。
- 陣列中的整數範圍是[-100,100]。
- 給定陣列中可能包含重複數字,相等的數字應該被視為遞增的一種情況。
本體最大的問題在於給的陣列是沒有排序的,我們之前的排序後樹層去重解決不了相同元素不在一起的情況,比如[1,2,3,1,1,1]
所以在每一層都設定一個set,專門去除重複元素。
class Solution { // 定義全域性變數儲存結果 List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> findSubsequences(int[] nums) { // idx 初始化為 -1,開始 dfs 搜尋。 dfs(nums, -1, new ArrayList<>()); return res; } private void dfs(int[] nums, int idx, List<Integer> curList) { // 只要當前的遞增序列長度大於 1,就加入到結果 res 中,然後繼續搜尋遞增序列的下一個值。 if (curList.size() > 1) { res.add(new ArrayList<>(curList)); } // 在 [idx + 1, nums.length - 1] 範圍內遍歷搜尋遞增序列的下一個值。 // 藉助 set 對 [idx + 1, nums.length - 1] 範圍內的數去重。 Set<Integer> set = new HashSet<>(); for (int i = idx + 1; i < nums.length; i++) { // 1. 如果 set 中已經有與 nums[i] 相同的值了,說明加上 nums[i] 後的所有可能的遞增序列之前已經被搜過一遍了,因此停止繼續搜尋。 if (set.contains(nums[i])) { continue; } set.add(nums[i]); // 2. 如果 nums[i] >= nums[idx] 的話,說明出現了新的遞增序列,因此繼續 dfs 搜尋(因為 curList 在這裡是複用的,因此別忘了 remove 哦) if (idx == -1 || nums[i] >= nums[idx]) { curList.add(nums[i]); dfs(nums, i, curList); curList.remove(curList.size() - 1); } } } }
排序問題:
排序而言,[1,2]和[2,1]是兩種情況,此時遞迴樹長這樣:
排序問題一:
給定一個 沒有重複 數字的序列,返回其所有可能的全排列。
示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]