来源:代码随想录
回溯本质是穷举,有的题目,高效的穷举已经是最有效的办法了
回溯法,一般可以解决如下几种问题:
排列对比于组合前调顺序
回溯可以抽象为一种树形结构(N叉树)
回溯三步:
回溯算法中函数返回值一般为void。
再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
void backtracking(参数)
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
if (终止条件) {
存放结果;
return;
}
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
有选择,选择后需要回溯,撤销处理结果
递归(向下)和回溯(撤销向上)相辅相成
完整模版
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
for循环嵌套可以写 选择大小为2的子集,但是当子集变大时,(e.g100)暴力for循环嵌套实现很不实际。
来源:代码随想录
用回溯算法要容易得多。
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
图中每次搜索到了叶子节点,我们就找到了一个结果。
1.返回值和参数:
backtrack返回值一般为0
参数:
函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。
然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
使用startIndex的原因:由于是组合问题,可以直接靠startindex来避免加入的树重复(不需要visit),如图(来源同上)
2. 终止条件:
到根节点了,说明找到了符合条件的数组
保存的时候记得new一个path加入,否则会随着递归一直改变(引用问题)
3. 单层递归的逻辑:
在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
如此我们才遍历完图中的这棵树。
剪枝优化: 回溯算法可以通过剪枝来提高效率
当可选集数量不够满足结果子集所需长度时,遍历没有意义了
如图:
所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程如下:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中最多从开始的i遍历到 : n - (k - path.size()) + 1位置
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
弄不清楚的话可以举个例子
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
最终java 代码:
class Solution {
public List> combine(int n, int k) {
List> res = new LinkedList<>();
LinkedList path = new LinkedList<>();
backtrack(path,res,1,n,k);
return res;
}
public void backtrack(LinkedList path,List> res,int start,int n, int k){
if(path.size()==k){
res.add(new LinkedList(path));
return;
}
// if(start+k-1>n)return;
for(int i= start;i<=n-(k-path.size())+1;i++){
path.addLast(i);
backtrack(path,res,i+1,n,k);
path.removeLast();
}
}
}