今天开始回溯法的学习,首先要明确回溯和递归是分不开的,且回溯一般将递归返回值设置为void。
1.组合(力扣77)
本题主要用来熟悉回溯的使用,首先递归函数返回值为void,然后首先判断是否满足返回条件,满足条件则放入结果集,然后返回,返回时回溯,否则递归调用。
class Solution { List<Integer> list = new ArrayList<>(); List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) { int startIndex = 1; search(n,k,startIndex); return res; } public void search(int n,int k,int startIndex){ if(list.size() == k) { // list.add(startIndex); res.add(new ArrayList<>(list)); // list.remove(list.size()-1); return; } for (int i = startIndex; i<=n ; i++) { list.add(i); search(n,k,i+1); list.remove(list.size()-1); } return; } }
2.组合总和3(力扣216)
按照回溯的基本方法,当满足条件时返回。
class Solution { List<Integer> list = new ArrayList<>(); List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> combinationSum3(int k, int n) { search(k,n,1); return res; } public void search(int k,int n,int startIndex){ if(k == 0 && n == 0){ res.add(new ArrayList<>(list)); return; } for (int i = startIndex; i <= 9; i++) { list.add(i); search(k-1,n-i,i+1); list.remove(list.size()-1); } } }
*3.电话号码的字母组合(力扣17)
由于本题是多对多的组合,每次应该从新的字符串的第一个位置起遍历,不能像上两道题一样用startIndex起遍历。本题利用回溯解决了n个for循环的问题。
class Solution { List<String> res = new ArrayList<>(); StringBuilder sb = new StringBuilder(); String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; public List<String> letterCombinations(String digits) { if (digits.length() == 0) return res; search(digits,0); return res; } public void search(String digits,int num){ if(sb.length() == digits.length()){ res.add(sb.toString()); return; } String str = numString[digits.charAt(num)-'0']; for (int i = 0; i < str.length(); i++) { sb.append(str.charAt(i)); search(digits,num+1); sb.deleteCharAt(sb.length()-1); } } }
4.组合总和(力扣39)
本题仍然采用回溯法遍历所有情况,但由于数组中元素可以重复出现,则startIndex不用每次递归时加一(之前的情况,用过的元素不能再用,因此要startIndex要每次递归时加一)。
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { search(candidates, target, 0); return res; } public void search(int[] candidates, int target, int startIndex){ if(target==0) { res.add(new ArrayList<>(list)); return; } if(target<0) return; for (int i = startIndex; i < candidates.length; i++) { list.add(candidates[i]); search(candidates,target-candidates[i], i);//注意startIndex不用是在原来基础上加一了,因为可以重复读当前元素 list.remove(list.size()-1); } } }
5.组合总和2(力扣40)
注意本题既有重复元素出现,又要求每个在给定数组中的元素只出现一次。当一个数判断完所有情况后直接跳到与其不相等的数开始下一轮。
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); search(candidates,target,0); return res; } public void search(int[] candidates, int target, int startIndex){ if (target<0) return; if(target == 0) { res.add(new ArrayList<>(list)); return; } for (int i = startIndex; i < candidates.length;i++) { if (candidates[i]>target) return; list.add(candidates[i]); search(candidates,target-candidates[i],i+1); list.remove(list.size()-1); //跳到与这个数不相等的数 if(i<candidates.length-1 && candidates[i] == candidates[i+1]){ while (candidates[i] == candidates[i+1]){ i++; if(i == candidates.length-1) break; } } } } }
6.分割回文串(力扣131)
本体有一定难度,利用回溯来切割字符串,符合的放入list,切割到最后时将list放入res。
class Solution { List<List<String>> res = new ArrayList<>(); List<String> list = new ArrayList<>(); public List<List<String>> partition(String s) { search(s,0); return res; } public void search(String s,int startIndex){ if(startIndex >= s.length()) { res.add(new ArrayList<>(list)); return; } for (int i = startIndex; i < s.length(); i++) { //注意切割 if(isHuiwen(s.substring(startIndex,i+1))){ String str = s.substring(startIndex, i + 1); list.add(str); search(s,i+1); list.remove(list.size()-1); } } } public boolean isHuiwen(String s){ if(s.length() == 0) return false; if(s.length()==1) return true; int i = 0; while (i <= (s.length()/2)) { if(s.charAt(i) == s.charAt(s.length()-1-i)) i++; else return false; } return true; } }
7.复原IP地址(力扣93)
本题依然是字符串切割,利用回溯,然后记住IP地址最多有三位,即时剪枝,还有注意前导0的问题。
class Solution { List<String> res = new ArrayList<>(); StringBuilder sb = new StringBuilder(); public List<String> restoreIpAddresses(String s) { search(s,0,4); return res; } public void search(String s, int startIndex, int point){ if(startIndex == s.length() && point == 0) { res.add(sb.toString()); return; } if(point == 0) return; for (int i = startIndex; i < s.length(); i++) { //最多有3位 if(i-startIndex>3) return; //不能含有前导0 if(s.substring(startIndex,i+1).length()>1&&s.substring(startIndex,i+1).charAt(0)=='0') return; int val = Integer.parseInt(s.substring(startIndex,i+1)); String str; if(val >= 0 && val <= 255){ if(point>1) str = s.substring(startIndex,i+1)+"."; else str = s.substring(startIndex,i+1); sb.append(str); search(s,i+1,point-1); sb.delete(sb.length()-str.length(),sb.length()); } } } }
8.子集(力扣78)
本题由于求子集,需要把所有元素都收集,则我们在过程中就收集,而不是最后才收集。
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { search(nums,0); return res; } public void search(int[] nums, int startIndex){ //这里注意在过程中收集,而不是到最后收集 res.add(new ArrayList<>(list)); if(startIndex == nums.length){ return; } for (int i = startIndex; i < nums.length; i++) { list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); } } }
9.子集2(力扣90)
本题与上题的不同在于数组有重复,而结果需要去重,因此在回溯后要将下一次取的位置直接跳到与此位置元素不等的元素的位置。(前提是将数组排序,重复的连在了一起)
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> subsetsWithDup(int[] nums) { //对数组排序可以使重复的元素连续出现 Arrays.sort(nums); search(nums,0); return res; } public void search(int[] nums, int startIndex){ res.add(new ArrayList<>(list)); if(startIndex == nums.length) return; for (int i = startIndex; i < nums.length; i++) { list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); if(i<nums.length-1 && nums[i] == nums[i+1]){ while (nums[i] == nums[i+1]){ i++; if(i == nums.length-1) break; } } } } }
10.递增子序列(力扣491)
本题由于数组无序且不能排序,则与上题又有区别(不能让重复的连续出现,增加了去重难度),采用set去重,保证每轮出现过的数不再使用,感谢腾爷的分享!
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> findSubsequences(int[] nums) { search(nums,0); return res; } public void search(int[] nums, int startIndex){ if(list.size() >= 2){ res.add(new ArrayList<>(list)); } //每一轮都有set,保证该轮出现过的数不再出现。 Set<Integer> set = new HashSet<>(); for (int i = startIndex; i < nums.length; i++) { if(list.size() == 0){ if(set.contains(nums[i])){ continue; } set.add(nums[i]); list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); } else if ( nums[i] >= list.get(list.size() - 1)) { if(set.contains(nums[i])){ continue; } set.add(nums[i]); list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); } } } }
11.全排列(力扣46)
本题求全排列,与组合问题不同,每次开始的位置应该是数组第一个位置,且数组中的元素不能重复出现,因此在list.add()时加以限制,使其中有过的元素不再加入。
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); public List<List<Integer>> permute(int[] nums) { search(nums,0); return res; } public void search(int[] nums,int index){ if(list.size() == nums.length) { res.add(new ArrayList<>(list)); return; } for (int i = 0; i < nums.length; i++) { //如果该元素用过,则跳过 if(list.contains(nums[i])) continue; list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); } } }
12.全排列2(力扣47)
本题在上一题的基础上加入了重复的元素,为了避免一个元素使用多次,采用HashMap存放元素和其出现的次数,次数为零时不能使用,若不为零则使用时次数减一,回溯时次数加一,同时在回溯后也要保证与本轮元素相同的元素不再进入下一轮循环。
class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); HashMap<Integer,Integer> hashMap = new HashMap<>(); public List<List<Integer>> permuteUnique(int[] nums) { //排序保证后面的去重简单(与本轮元素相同的元素直接跳过循环) Arrays.sort(nums); //HashMap统计频数 for (int i = 0; i < nums.length; i++) { hashMap.put(nums[i], hashMap.getOrDefault(nums[i],0)+1); } search(nums,0); return res; } public void search(int[] nums, int index){ if(list.size() == nums.length) { res.add(new ArrayList<>(list)); return; } for (int i = 0; i < nums.length; i++) { if (hashMap.get(nums[i])==0) continue; hashMap.put(nums[i],hashMap.get(nums[i])-1); list.add(nums[i]); search(nums,i+1); list.remove(list.size()-1); hashMap.put(nums[i],hashMap.get(nums[i])+1); if(i<nums.length-1 && nums[i] == nums[i+1]){ while (nums[i] == nums[i+1]){ i++; if (i == nums.length-1) break; } } } } }