代码随想录算法训练营第三十八天 | LeetCode 509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

简介: 代码随想录算法训练营第三十八天 | LeetCode 509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

代码随想录算法训练营第三十八天 | LeetCode 509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

文章链接:斐波那契数        爬楼梯        使用最小化费爬楼梯

视频链接:斐波那契数        爬楼梯        使用最小化费爬楼梯

1. 动态规划理论基础

1.1 动态规划能解决什么题目

  • 动规基础
  • 背包问题
  • 打家劫舍
  • 股票问题
  • 子序列问题

1.2 什么是动态规划

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

所以贪心解决不了动态规划的问题。

其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了。

大家只要知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的。

1.3 动态规划的解题步骤

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例打印推导dp数组

一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

1.4 动态规划如何debug

找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!

很多人对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。

然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。

如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。

如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题

如果发现自己的代码和题解一样但是通过不了,就灵魂三问:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。

以上关于动态规划的理论基础来源于代码随想录Carl

2. LeetCode 509. 斐波那契数

2.1 思路

  1. 简单题有很多方法直接上动态规划步骤:
  2. 确定 dp[i] 数组含义及下标:dp[i] 第 i 个斐波那契数为 dp[i]
  3. 递推公式:dp[i]=dp[i-1]+dp[i-2]。这题简单就简单在这里题目已经告诉了
  4. dp 数组如何初始化:dp[0]=0;dp[1]=1
  5. 遍历顺序:从前向后遍历,这样才能保证 dp[i] 是由 dp[i-1] 和 dp[i-2] 构成的
  6. 打印 dp 数组:主要用来 debug
  7. 注意:我们创建 dp 数组的长度是 n+1,因为 n+1 是数组长度,下标是 0~n。然后 for 循环遍历里 i<dp.length 也可以,i<=n 也可以,因为其实第 n 个斐波那契数是在 dp[n] 这个位置的,这也就是为什么我们要从定义长度为 n+1,遍历时 <=n 的原因。最后是 return dp[n]
  8. 也可以不用定义数组,就定义三个变量,a=0,b=1,c=0;在 for 循环中 c=a+b;a=b;b=c。最后 return c 即可

2.2 代码

//
//非压缩状态的版本
class Solution {
    public int fib(int n) {
        if (n <= 1) return n;             
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for (int index = 2; index <= n; index++){
            dp[index] = dp[index - 1] + dp[index - 2];
        }
        return dp[n];
    }
}
//压缩状态的版本
class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        int a = 0, b = 1, c = 0;
        for (int i = 1; i < n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

3. LeetCode 70. 爬楼梯

3.1 思路

  1. 有 n 阶楼梯,爬了 n 阶楼梯到达楼顶有多少种方法。举例推导,假设是 1 阶,从 0 到 1,1 种方法。2 阶,从 0 到 2,从 1 到 2,2 种方法。3 阶,从 1 到 3,从 2 到 3,不能从 0 到 3,而 1 阶、2 阶和 3 阶有什么关系,就是前 2 种方法相加,因此是 3 种。不要混淆,走到 1 阶有 1 种方法,从 1 阶再走到 3 不是另一种方法,不是 1+1=2,误认为 2 的是因为把方法理解成步数了
  2. 确定 dp 数组及下标的含义:达到 i 阶有 dp[i] 种方法。
  3. 确定递推公式:dp[i] 是由前两阶的方法相机的,dp[i]=dp[i-1]+dp[i-2]
  4. 初始化 dp 数组:注意题目的 n 给的是正整数,n 不为 0。 因此 dp[0] 没有意义,因为我们 dp[i] 的含义是"达到 i 阶有 dp[i] 种方法"。因此初始化 dp[1]=1,dp[2]=2 即不考虑 0 阶的情况
  5. 遍历顺序:只有从前向后遍历,这样才能保证 dp[i] 是由 dp[i-1] 和 dp[i-2] 相加构成的
  6. 打印 dp 数组:自己推导模拟一下后面几阶。
  7. 这题和509. 斐波那契数的区别在于 0 下标的 dp[0] 是没意义的

3.2 代码

//
// 常规方式
public int climbStairs(int n) {
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}
 用变量记录代替数组
class Solution {
    public int climbStairs(int n) {
        if(n <= 2) return n;
        int a = 1, b = 2, sum = 0;
        for(int i = 3; i <= n; i++){
            sum = a + b;  // f(i - 1) + f(i - 2)
            a = b;        // 记录f(i - 1),即下一轮的f(i - 2)
            b = sum;      // 记录f(i),即下一轮的f(i - 1)
        }
        return b;
    }
}

4. LeetCode 746. 使用最小花费爬楼梯

4.1 思路

  1. 模糊点 1:比如 [10,15,20],假设我们是站在下标 0 的位置,消耗的是 10,那我是站在 0 位置花费 10 还是从 0 开始跳上去后花费 10 呢?是后者,站在上面是不花费的。
  2. 模糊点 2:楼顶在哪?比如 [10,15,20],我们的楼顶是下标 2 的位置吗?不是,因此应该是下标 3 的位置。这题相当于70. 爬楼梯的消费版
  3. 确定 dp 数组及下标的含义:dp[i] 表示到达 i 位置最小花费为 dp[i]
  4. 递推公式:dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]),回顾一下 dp[i] 的意思,到达 dp[i] 位置的最小花费,要想到达下一步就要花费对应的 cost[i] 才行,而我们要的是最小化费,因此需要取两者最小值
  5. 初始化 dp 数组:由于题目说可以从下标 0 和 1 的位置开始,往上跳的时候才花费对应位置的体力。因此 dp[1] 和 dp[0] 都初始化为 0,因为我们从这两个位置开始是不花费体力的,往上跳才花费
  6. 遍历顺序:正常的从前往后
  7. 打印 dp 数组:用于 debug,看看是不是按照我们预想的出来的

4.2 代码

//
// 方式一:第一步不支付费用
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;
        int[] dp = new int[len + 1];
        // 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0
        dp[0] = 0;
        dp[1] = 0;
        // 计算到达每一层台阶的最小费用
        for (int i = 2; i <= len; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[len];
    }
}
相关文章
|
6天前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
9天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
17天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
18 3
|
16天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
29天前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
30天前
|
算法
每日一道算法题(Leetcode 20)
每日一道算法题(Leetcode 20)
23 2
|
30天前
|
搜索推荐
插入排序算法的讲解和代码
【10月更文挑战第12天】插入排序是一种基础的排序算法,理解和掌握它对于学习其他排序算法以及数据结构都具有重要意义。你可以通过实际操作和分析,进一步深入了解插入排序的特点和应用场景,以便在实际编程中更好地运用它。
|
22天前
|
缓存 分布式计算 监控
优化算法和代码需要注意什么
【10月更文挑战第20天】优化算法和代码需要注意什么
16 0
|
24天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
9天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。