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

本文深入探讨了回溯算法在解决组合问题中的应用,通过将问题抽象为树形结构,阐述了回溯法的三部曲——递归函数参数、终止条件和单层搜索过程。以77.组合和216.组合总和III为例,展示了如何使用递归避免大规模的for循环嵌套,以及如何利用startIndex参数调整搜索范围。此外,还对比了递归和for循环在处理此类问题上的优劣,并提供了具体的Java代码实现。最后强调了在面对递归问题时绘制递归树的重要性,以辅助理解算法复杂度和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

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

77. 组合
216. 组合总和 III

在这里插入图片描述

简单实例:

 * 给定两个整数 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);
            }
        }
    }
}

在这里插入图片描述
递归函数的返回值一般都是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. 单层搜索的过程

我们把组合问题抽象为如下树形结构
在这里插入图片描述

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

在这里插入图片描述

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

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

为什么要有这个startIndex呢?

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

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

所以需要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);
        }
    }
}

参考链接:

https://blog.youkuaiyun.com/qq_45759413/article/details/121954516
代码随想录

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值