回溯算法就是解决这种k层for循环嵌套的问题
回溯算法的搜索过程抽象为树形结构,可以直观的看到搜索的过程
接着使用回溯三部曲,逐步分析函数参数、终止条件、单层搜索的过程

回溯法虽然是暴力搜索,但是有时也是可以减枝优化的

PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。
​​​ https://leetcode-cn.com/problems/fibonacci-number/solution/dong-tai-gui-hua-tao-lu-xiang-jie-by-labuladong/​

  1. 组合

216. 组合总和 III

77. 组合(递归回溯操作步骤)_leetcode

简单实例:

* 给定两个整数 n=5  k=2,返回范围 [1, 5] 中所有可能的 2 个数的组合。
* 你可以按 任何顺序 返回答案。

public class test {
public static void main(String[] args) {
for (int i=1;i<5;i++){
for (int j=i+1;j<5;j++){
System.out.println(i+","+j);
}
}
}
}

77. 组合(递归回溯操作步骤)_for循环_02


递归函数的返回值一般都是void ,

只有在特殊情况,才会有返回值

就是在求回溯算法的时候

for循环是横向遍历
backtracking是纵向遍历
如果n为100,k为50呢,那就50层for循环,是不是开始窒息。

此时就会发现虽然想暴力搜索,但是用for循环嵌套连暴力都写不出来!

咋整?

回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。

那么回溯法怎么暴力搜呢?

上面我们说了要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯法就用递归来解决嵌套层数的问题。

递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。

此时递归的层数大家应该知道了,例如:n为100,k为50的情况下,就是递归50层。

如果脑洞模拟回溯搜索的过程,绝对可以让人窒息,所以需要抽象图形结构来进一步理解。

回溯法三部曲

  1. 递归函数的返回值以及参数
  2. 回溯函数终止条件
  3. 单层搜索的过程

我们把组合问题抽象为如下树形结构

77. 组合(递归回溯操作步骤)_搜索_03

n为树宽,
k为树的深度
叶子节点即为我们需要的结果

77. 组合(递归回溯操作步骤)_搜索_04

单层搜索过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历

77. 组合(递归回溯操作步骤)_搜索_05


然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,…,n] )。

为什么要有这个startIndex呢?

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex。

从下图中红线部分可以看出,在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。

77. 组合(递归回溯操作步骤)_for循环_06

所以需要startIndex来记录下一层递归,搜索的起始位置。

class Solution {
public List<List<Integer>> combine(int n, int k) {

return combineList(new ArrayList<>(), 1, n, k);
}

public static List<List<Integer>> combineList(List<Integer> list, int num, int n, int k) {
List<List<Integer>> allList = new ArrayList<>();
if (list.size() == k) {
allList.add(list);
return allList;
}
for (int i = num; i < (n - k + 1 + list.size() + 1); i++) {
List newList = new ArrayList();
newList.addAll(list);
newList.add(i);

allList.addAll(combineList(newList, i+1, n, k));
}
return allList;
}
}
class Solution {

//定义两个集合,一个用来存放最终汇总的结果
//一个用来存放每次符合条件的结果
//每次符合条件的结果, 其集合数据是整型
//最终汇总的结果,其数据是集合
List<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();

public List<List<Integer>> combine(int n, int k) {
// 首先看到这个题,想到组合问题就是回溯中的一种
// 使用回溯,回溯三部曲。一般用递归,没有返回值
backtracking(n,k,1);//执行回溯,将结果都放到集合res中
//回溯就是暴力求解,没有重叠子问题,列出所有可能的集合
return res;//返回res
}
//参数可以晚点确认,一边写,一边确定参数
//首先集合中有两个参数,n代表最大范围,k代表选几个数,index代表从哪里开始
public void backtracking(int n,int k,int index){
//确认参数和返回值
//确认终止条件
//什么时候达到叶子节点就结束,同时将数据收集起来
if(path.size()==k){
//开辟一块新的地址,地址的内容为当前path的内容
//之后path再怎么变,都不会影响了
//组合3,这里多了个条件,就是多了一个收集结果的条件
res.add(new LinkedList<>(path));
return;
}
//确认单层递归逻辑,暴力穷举所有可能
for(int i=index;i<=n;i++){
//进行此层的操作
path.add(i);
//u1s1,debug真的好用
// System.out.println(path);
//递归
//从剩余节点开始选,n代表范围,k代表选几个数,index代表从哪里开始
//开始第二层的操作
backtracking(n,k,i+1);
//回撤,回溯,撤销对节点的处理,然后返回上层继续
//此时已经到达叶子节点,需要回撤,在第二层循环中再选择一个数,到达叶子节点
//移出第二层,返回第一层,并继续添加新节点到path,因为上一个path是new,所以,这个新path是新的
//返回上一层操作
path.remove(path.size()-1);
}
}
}

参考链接:

​代码随想录​

res.add(new ArrayList<>(path))和res.add(path)的区别