理论基础
如何理解回溯法
回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
例如下面这一张图片,与下一个要讲的题目有关,也是先抽象成一个树形结构再进行代码的构建
回溯法模板
回溯法其实就是用递归写的,每一个递归函数都会存在回溯的过程,只是有些递归可能没有用到这个过程而已,大致与之前写过打的一道遍历n叉树节点的题目很像
大致如下,之后解题大部分都是用的这个模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
思路:
像这一题我们并不难想到,有多少个元素就用多少层for嵌套输出答案即可,但是我们代码不可以想套几个就套几个,因为在编译的时候代码已经是写好了,所以这里我们要用递归代替这随机的for循环嵌套次数
先用n叉树模拟过程
根据上面说的模板我们可以写成下面代码
List<List<Integer>> res;
LinkedList<Integer> path;//保存路径
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<>();
path = new LinkedList<>();//使用双链表便于尾插尾删
Combine(n,k,1);
return res;
}
/*接下来看一下优化过程如下:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0)
,n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。*/
//剪枝优化过后
public void Combine(int n, int k,int starIndex){
//一组元素量到达k就收集结果
if(path.size() == k){
//保留结果
res.add(new ArrayList<>(path));
return;
}
//没有达到就继续向前遍历
for(int j = starIndex;j<=n-(k-path.size())+1;j++){
path.add(j);//处理节点
Combine(n,k,j+1);//递归
//回溯
path.removeLast();
}
}
代码中还包含的一段对于剪枝操作的注解,感兴趣可以看看
感谢大家观看!