理解回溯
回溯常常伴随着递归,回溯操作都是在递归函数的下面进行操作的。所有的回溯问题都可以抽象成一个多叉树来解决。我们之前在对二叉树进行递归遍历的时候,其实就有回溯过程,只是我们没有处理(因为不需要),但是例如找二叉树的路径的题目就涉及到回溯了。
解决回溯问题的思路:
第一步:抽象成多叉树
第二步:确定多叉树的宽度和深度
第三步:确定递归函数的终止条件
第四步:递归函数单层递归逻辑,包含回溯操作
组合问题
题目:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
- 抽象成多叉树
- 宽度就是n,体现在递归函数中的循环
- 深度是k,体现在终止条件
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
backtracking(n,k,1,path,res);
return res;
}
// startIndex:递归中开始位置
private void backtracking(int n,int k,int startIndex,List<Integer> path,List<List<Integer>> res){
if(path.size() == k){// 终止条件,当我们需要的组合的集合大小等于k的时候就可以收集结果了
res.add(new ArrayList<>(path));// 这里不能直接add(path),拷贝一份path放进去,如果直接放进去那么res中的所有元素都是path对象
return;
}
for(int i = startIndex;i<=n;i++){
path.add(i);
backtracking(n,k,i+1,path,res);
path.remove(path.size() - 1);// 回溯
}
}
}
剪枝:主要就是缩小for循环的范围,例如n = 7,k = 3;那么如果循环中的i>5,后面就怎么也收集不到3个,所有5就是for循环的搜索的截止下标(包括5)
怎么计算这个截止值呢?
首先我们可以带入数据计算,例如当前一个都没有收集,n至少需要剩余3个元素才能满足条件。那么截止下标=n-k+1,为什么要+1呢,因为我们for循环是从1开始的不是从0开始的。如果我们收集了部分元素,就用k-x,就行了;就是n-(k-x)+1
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
backtracking(n,k,1,path,res);
return res;
}
// startIndex:递归中开始位置
private void backtracking(int n,int k,int startIndex,List<Integer> path,List<List<Integer>> res){
if(path.size() == k){// 终止条件,当我们需要的组合的集合大小等于k的时候就可以收集结果了
res.add(new ArrayList<>(path));// 这里不能直接add(path),拷贝一份path放进去,如果直接放进去那么res中的所有元素都是path对象
return;
}
// 当前递归最多遍历到lastIndex
// 例如path大小为0,lastIndex=3;如果循环中的i大于了3,后面只有一个4了,加上3才满足k=2,如果i在往后走的话,后面就可能满足k=2了
int lastIndex = n - (k - path.size()) + 1;
for(int i = startIndex;i<=lastIndex;i++){
path.add(i);
backtracking(n,k,i+1,path,res);
path.remove(path.size() - 1);// 回溯
}
}
}