回溯二刷:
组合总和:nums无重复元素---同一层不可重复取---纵向可以重复取
nums无重复元素--所以横向不用去重
但纵向重复取?---单层循环中,i不能从0开始。否则:(2,2,3)与(3,2,2)
不能从i+1开始(下一层递归传入的),否则取不到重复的
组合总和ii:nums有重复元素---同一层不可重复取---纵向可以重复取(因为有重复元素)
纵向可以重复取(因为有重复元素)?----从i+1开始(下一层递归传入的)
横向去重---排序,相同的跳过
组合总和ii:
回溯三刷:
组合剪枝问题:
电话号码的组合:字符串的处理问题
递增子序列:不排列的情况下,使用哈希对同一层去重
一:回溯算法
纯暴力搜素算法
为什么还要用?能搜出来就不错
为什么不用for嵌套?n层排列组合--n个for嵌套,代码很繁琐
而且有的时候n是变长,无法用for嵌套
这个时候只能用回溯算法暴力解出来
回溯本质还是用了递归函数--10个数字排列组合-要找第10个数字,先找到前9个数字的(10的排列组合)
1.解决的问题
无法用for嵌套,只能回溯法暴力搜素出来
2.回溯模板
为什么要有回溯操作?
如下:我们在1后面加了2,要回溯(撤回加2这个操作),又变回1,
后面加3,就可以实现13, 否则会一直加下去,变成123
二.组合问题
77题
for循环部分为单层搜素逻辑,后面的剪枝部分也只优化这部分
无剪枝下,单层循环里,i<=n
剪枝下,i<=n-(k-path.size)+1
组合问题有重复的,1234 与 4321 是相同,剪枝掉4321
3.77组合
class Solution { List<List<Integer>> ans = new ArrayList<>();//返回结果 private LinkedList<Integer> path=new LinkedList<>();//路径 public List<List<Integer>> combine(int n, int k) { backTrace(n, k, 1); return ans; } public void backTrace(int n,int k,int startIndex){ //终止条件 if (k==path.size()) { ans.add(new ArrayList<>(path)); return ; } //单层处理, i <= n-(k-path.size())+1剪枝操作 for (int i = startIndex; i <= n-(k-path.size())+1; i++) { path.addLast(i);//添加元素 backTrace(n, k, i+1);//递归 path.removeLast(); //回溯 } } }
4.组合总和iii
纵:递归次数
横:单层里的for循环次数
剪枝:
class Solution { List<List<Integer>> ans =new ArrayList<>(); ArrayList<Integer> path =new ArrayList<>(); public List<List<Integer>> combinationSum3(int k, int n) { backTracking(n, k, 1, 0); return ans; } public void backTracking(int sum,int k,int index, int cur_sum){ //两个终止条件 if (cur_sum>sum) return; if (k==path.size()) { if (cur_sum==sum) { ans.add(new ArrayList<>(path)); return; } return; } //单层逻辑 for (int i = index; i <= 9-(k-path.size())+1; i++) { path.add(i); cur_sum+=i; backTracking(sum, k, i+1, cur_sum); cur_sum-=i; path.removeLast(); } } }
5.电话号码的字母组合
class Solution { List<String> ans=new ArrayList<>();//返回结果 StringBuilder path =new StringBuilder();//保存每次回溯的最终结果 public List<String> letterCombinations(String digits) { String [] numStrings={"","","abc","def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; if (digits.length()==0||digits==null) { return ans; } backTracking(digits, 0, numStrings); return ans; } public void backTracking(String digits, int index,String [] numStrings){ if (index==digits.length()) { ans.add(path.toString()); return ; } //处理digits,index层数,纵向,i横向 String curStr=numStrings[digits.toCharArray()[index]-'0']; // 单层循环i : 0--> curStr.length(),该层的curStr的字符都遍历 for (int i = 0; i < curStr.length(); i++) { path.append(curStr.charAt(i));//处理节点 backTracking(digits, index+1, numStrings);//递归 path.deleteCharAt(path.length()-1);//回溯,删除最后一个 } } }
6.组合总和
有重复的,没剪枝
class Solution { List<List<Integer>> ans =new ArrayList<>(); ArrayList<Integer> path =new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { backTracking(target, candidates, 0,0); return ans; } public void backTracking(int target,int[] candidates,int cur_sum,int index){ if (cur_sum>target) return ;//终止条件 if (cur_sum==target) { ans.add(new ArrayList<>(path)); return ; } for (int i = index; i < candidates.length; i++) { //从0开始有重复,前面的重复了 path.add(candidates[i]); cur_sum+=candidates[i]; // 关键点:不用i+1了,表示可以重复读取当前的数 // 递归下去可以重复取后面的数 backTracking(target, candidates, cur_sum,i); cur_sum-=candidates[i]; path.removeLast(); } } }
7.组合总和ii--树去重
index是纵向的指标
i是横向的指标
class Solution { List<List<Integer>> ans = new ArrayList<>(); ArrayList<Integer> path = new ArrayList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) { // 为了将重复的数字都放到一起,所以先进行排序 Arrays.sort(candidates); backTracing(target, candidates, 0, 0); return ans; } public void backTracing(int target, int[] candidates, int cur_sum, int index) { if (target < cur_sum) return;// 终止条件 if (target == cur_sum) { ans.add(new ArrayList<>(path)); return; } for (int i = index; i < candidates.length; i++) { // 正确剔除重复解的办法 // 跳过同一树层使用过的元素 if (i > index && candidates[i] == candidates[i - 1]) { continue; } path.add(candidates[i]); cur_sum += candidates[i]; backTracing(target, candidates, cur_sum, i + 1); cur_sum -= candidates[i]; path.removeLast(); } } }
三:分割问题
8.131. 分割回文串
分割思想:
回溯实现分割代码:
终止条件:
能走到最后的都是符合的,不符合的提前停止了
单层逻辑:
是回文子串才切割
判断回文子串:
class Solution {
List<List<String>> ans =new ArrayList<>();
ArrayList<String> path =new ArrayList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return ans;
}
public void backTracking(String s,int index_start){
if (index_start>=s.length()) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = index_start; i < s.length(); i++) {
if (isPali(s, index_start, i)) {//是回文,切割
// 获取[startIndex,i]在s中的子串
String str =s.substring(index_start,i+1 );
path.add(str);
}else{continue;} //不是,直接跳过,不切割
backTracking(s, i+1);
path.removeLast();
}
}
public boolean isPali(String s,int start,int end){
for (int i = start,j=end; i < j; i++,j--) {
if (s.charAt(i)!=s.charAt(j)) return false;
}
return true;
}
}
9.93复原 IP 地址
四:子集
图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
每一层都有保存结果
之前只保存最后一层的结果
存放结果在最上面,一进入下一层,九巴上一层收集到的结果存起来
9.78. 子集
class Solution {
List<List<Integer>> ans = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums, 0);
return ans;
}
public void backTracking(int[] nums,int start){
ans.add(new ArrayList<>(path));
if(start>=nums.length) return ;//终止条件
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backTracking(nums, i+1);
path.removeLast();
}
}
}
10.90. 子集 II--树去重
不能有[1,2,1],[1,1,2].
树去重
class Solution {
List<List<Integer>> ans = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
if (nums.length==0) {
ans.add(path);
return ans;
}
//排序,重复数字放一块
Arrays.sort(nums);
backTracking(nums, 0);
return ans ;
}
public void backTracking(int [] nums, int start){
ans.add(new ArrayList<>(path));//每一层的节点结果都保存
if (start>=nums.length) return;
for (int i = start; i < nums.length; i++) {
if (i>start&&nums[i]==nums[i-1]) {
continue; //去重
}
path.add(nums[i]);
backTracking(nums, i+1);
path.removeLast();
}
}
}
11.递增子序列
错误:
关于树去重:
纵向(树枝)上是可以取重复的
横向同一层级不可以
错误2:if (i>start&&nums[i]==nums[i-1]) {
continue; //去重,横向
}横向去重这块:,因为num里面是无序的,比较的不是前后两个元素,而是当前num[i]是否出现在path里
所以这里需要一个map映射,用于存储path里添加的元素,用map去维护去重
class Solution { List<List<Integer>> ans = new ArrayList<>(); ArrayList<Integer> path = new ArrayList<>(); public List<List<Integer>> findSubsequences(int[] nums) { backTracking(nums, 0); return ans ; } public void backTracking(int [] nums, int start){ //ans.add(new ArrayList<>(path));//每一层的节点结果都保 //类似分割,不保存长为1和空的 if(path.size() > 1) ans.add(new ArrayList<>(path)); if (start>=nums.length) return; for (int i = start; i < nums.length; i++) { if (i>start&&nums[i]==nums[i-1]) { continue; //去重,横向 } if (path.size()!=0&&nums[i]<path.getLast()) { continue; //,纵向,不满足递增,结束 } path.add(nums[i]); backTracking(nums, i+1); path.removeLast(); } } }
正确:用map去维护去重
替换:
五:排列
1.46. 全排列
纵向去重问题:
排列,有序
全排列:只保存最后一层
class Solution { List<List<Integer>> ans = new ArrayList<>(); LinkedList<Integer> path = new LinkedList<>(); public List<List<Integer>> permute(int[] nums) { backTracking(nums, 0); return ans ; } public void backTracking(int [] nums, int start){ //只保存最后一层 if(nums.length==path.size()){ ans.add(new ArrayList<>(path)); return; } // i从0开始,[1,2] [2,1]都遍历到 for (int i = 0; i < nums.length; i++) { if (path.size()!=0&&path.contains(nums[i])) { continue; //纵向去重 } path.add(nums[i]); backTracking(nums, i+1); path.removeLast(); } } }
2.47. 全排列 II
横向检查不能重复
纵向比较复杂,
力扣:括号生成
class Solution { // 全局变量,减少函数参数 List<String> list = new ArrayList<>(); public List<String> generateParenthesis(int n) { // String指向内容不可变,但是其引用可以变,所以我们用String // 不要用StringBuilder,虽然拼接快,但是回溯时删除效率低 String sb = ""; helper(sb,n,0,0); return list; } // l,r用来统计左右括号的数量 public void helper(String sb,int n,int l,int r){ if(l==n && r==n) { list.add(sb); return; } if(l>n||r>n||r>l) return; // 先左括号 helper(sb+"(",n,l+1,r); // 后右括号 helper(sb+")",n,l,r+1); return; } }
力扣:单词搜素--类似岛屿搜素
dfs这个函数,因为每一个点我们都可以往他的4个方向查找,所以我们可以把它想象为一棵4叉树,就是每个节点有4个子节点,而树的遍历我们最容易想到的就是递归,我们来大概看一下
力扣:八皇后
之前总结的递归和回溯