理论基础
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案。如果想要回溯高效一点,可以加些剪枝操作,但也不能改变回溯是穷举的本质。
那么既然回溯法并不高效为什么还要用它呢?
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,构成了树的深度。
是递归就有终止条件,所以必然是一颗高度有限的树(N叉树)。
回溯法模版:回溯算法中函数返回值一般为void;终止条件一般来说搜到叶子结点了,也就找到了满足条件的一条答案,把这个答案放起来,并结束本层递归;for循环可以理解为横向遍历,backtracking是纵向遍历。
void backtracking(参数) {
if(终止条件) {
存放结果;
return;
}
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果;
}
}
LeetCode 77. 组合
如果使用暴力搜索,当k很大时,用for循环嵌套连暴力都写不出来,例如,当n为100,k为50,就要写50层for循环,但可以使用回溯法用递归来解决嵌套层数的问题。每一次的递归中嵌套一个for循环,那么递归就可以解决多层嵌套循环的问题了。
在递归函数中,除了参数n和k还需要一个startIndex来记录本层递归中,集合从哪里开始遍历,startIndex可以防止出现重复的组合。当path数组的大小达到了k,说明找到了一个子集大小为k的组合,将其存放至结果集中。for循环每次从startIndex开始遍历([startIndex, n]),用path保存取到的节点i,再进行回溯操作,撤销本次处理结果。
本题还可以进行剪枝优化:如果for循环选择的起始位置之后的元素个数已经不足我们需要的元素个数了,那么就没必要搜索了。
1.已经选择的元素个数:path.size();
2.还需要的元素个数为:k - path.size();
3.在集合中最多遍历到该位置:n - ( k - path.size() ) + 1;
为什么要+1呢,因为包括起始位置,我们是一个左闭的集合。代码如下:
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
public void backtracking(int n, int k, int startIndex) {
if(path.size() == k) {
result.add(new ArrayList(path));
return;
}
for(int i=startIndex; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtracking(n, k, i+1);
path.removeLast();
}
}
}