代码随想录算法训练营第二十四天 | LeetCode 77.组合

简介: 代码随想录算法训练营第二十四天 | LeetCode 77.组合

1. 回溯算法理论基础

1.1 回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

回溯是递归的副产品,只要有递归就会有回溯。

因此,回溯函数也就是递归函数,指的都是一个函数。

1.2 回溯法的效率

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

但由于有些题目用一般的暴力求解也无法求解,能用回溯法暴力求解也已经很可以了。

1.3 回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

1.4 如何理解回溯法

回溯法解决的问题都可以抽象为树形结构。

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。(这句话很关键)

递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

1.5 回溯法的模板

回溯三部曲:

1.5.1 回溯函数模板返回值以及参数

函数起名字为backtracking,这是业内常用起名

返回值一般是void

参数一般是先写逻辑,然后需要什么参数,就填什么参数

void backtracking(参数)
1.5.2 回溯函数终止条件

什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。

if (终止条件) {
    存放结果;
    return;
}
1.5.3 回溯函数遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度

注意图中,我特意举例集合大小和孩子的数量是相等的!

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
}

for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

backtracking这里自己调用自己,实现递归。

大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

1.6 回溯算法模板框架

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

2. LeetCode 77.组合

2.1 思路

  1. 这题为什么用回溯算法?因为这题想用一般的暴力求解时,那层循环是由k(数位)决定,因此嵌套循环层数无法确定。那么回溯算法如何实现嵌套循环的效果的呢?其实回溯算法就是通过递归来控制有多少层for循环,递归的每一层就是一层for循环,往后一层就是下一层for循环。
  2. 由于回溯算法能解决的问题都可以抽象成一个树形结构。
  3. 这个树形结构中叶子节点就是我们要求的所有的组合。
    层数=组合大小=回溯深度。
    可能会差异为什么第二层中取2以后子集合只剩下“34”没有“1”了呢?如果还有1,那下面叶子节点就多一个“21”,就与“12”重复了,这就是“组合”与“排列”的区别。
    以下是回溯三部曲
  4. 回溯(递归)函数的参数和返回值:返回值一般都是void,函数名backtracking,参数呢?题目的答案中一个组合就是一个数组,把这个一维数组称为path,这题取数的过程就是求路径的过程,二维数组为result,这两个数组都定义为全局变量,参数需要n和k、startIndex,n表示n个数,k表示组合大小,startIndex,这个就是用来表示子集合为什么第一个是从2开始,第二个是从3开始,就是表示要搜索的起始位置
  5. 终止条件:到叶子节点就是结束了。怎么看出是到叶子节点呢?就是结果的大小长度就是组合的大小,就是如果path.length==k就可以result.add(path)然后return了
  6. 单层搜索(递归)的逻辑:每一个节点就是一个for循环,起始位置就是从startIndex开始的,然后遍历剩余的元素,for(int i=startIndex;i<=n;i++)注意这里是小于等于n,因为最开始startIndex是从1开始的。接着就是path把i加入到数组中。然后就是进入递归函数的过程,不同的是传入的startIndex要+1,也就是i+1。然后就是回溯的过程了,path要把元素弹出去,为什么呢?比如我们已经取到了“12”了,那么我们要取“13”前是不是要把“2”先弹出才能取到我们的“12”对吧。

2.2 代码

//
class Solution {
    List<List<Integer>> result= new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    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;i++){
            path.add(i);
            backtracking(n,k,i+1);
            path.removeLast();
        }
    }
}

2.3 剪枝优化思路

  1. 以这个为例子,第二层取2以后包括2也才剩下3个元素,怎么样也取不到4个数为组合了,后面也是一样,这些分支就可以做剪枝了,这个剪的枝如果不剪,那下面可能还有扩展,那么这个剪枝的效果就很明显了
  2. 回溯函数的参数和返回值:同上述普通思路
  3. 终止条件:同上述普通思路
  4. 单层搜索的逻辑:回溯算法通常都是递归函数里嵌套着for循环,而这题里每个节点都是for循环的过程。上面说到for(int i=startIndex;i<=n;i++),接着就是path把i加入到数组中。然后就是进入递归函数的过程。然后就是回溯的过程了,path要把元素弹出去。
  5. 而我们现在优化的就是要剪去节点的子孩子,有些没必要去搜索的,优化的地方就是在for循环里。
  6. 从上面的例子可以看出i=1可以继续往下搜索,i=2后面的都没必要搜索了,那我们知道path.size()是我们的当前已经选取结果的长度,我们还剩k-path.size()的元素要取,因为一共要取k个。那我们还要选取的元素至多可以让你从n-(k-path.size())+1开始搜索,+1是因为包括startIndex的位置。举个例子,4个元素选3个,最多让你从第2个元素开始选,所以就是4-3+1=2。k-path.size 表示path数组还能放几个元素, 再用n - 他们的和就是计算我从哪个下标开始能够满足path放k个元素
  7. 大多数回溯算法要做剪枝都是从这个i的范围做文章的,就是你思考是否可以做剪枝,就思考一下这个范围是不是有点大了,是否可以适当缩小一下

2.4 代码

//
class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        combineHelper(n, k, 1);
        return result;
    }
    /**
     * 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
     * @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
     */
    private void combineHelper(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);
            combineHelper(n, k, i + 1);
            path.removeLast();
        }
    }
}
相关文章
|
10天前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
13天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
22天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
25 3
|
21天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
1月前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
1月前
|
搜索推荐
插入排序算法的讲解和代码
【10月更文挑战第12天】插入排序是一种基础的排序算法,理解和掌握它对于学习其他排序算法以及数据结构都具有重要意义。你可以通过实际操作和分析,进一步深入了解插入排序的特点和应用场景,以便在实际编程中更好地运用它。
|
27天前
|
缓存 分布式计算 监控
优化算法和代码需要注意什么
【10月更文挑战第20天】优化算法和代码需要注意什么
18 0
|
28天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
5天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
13天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
下一篇
无影云桌面