代码随想录32 动态规划理论基础,509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯。
1.动态规划理论基础
动态规划刷题大纲
什么是动态规划
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,
在关于贪心算法,你该了解这些! (opens new window)中我举了一个背包问题的例子。
例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
所以贪心解决不了动态规划的问题。
其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了。
而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
上述提到的背包问题,后序会详细讲解。
动态规划的解题步骤
做动规题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。
这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中。
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?
因为一些情况是递推公式决定了dp数组要如何初始化!
后面的讲解中我都是围绕着这五点来进行讲解。
可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。
其实 确定递推公式 仅仅是解题里的一步而已!
一些同学知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。
后序的讲解的大家就会慢慢感受到这五步的重要性了。
动态规划应该如何debug
相信动规的题目,很大部分同学都是这样做的。
看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化,递推公式,遍历顺序,处于一种黑盒的理解状态。
写动规题目,代码出问题很正常!
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。
这是一个很不好的习惯!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了。
这也是我为什么在动规五步曲里强调推导dp数组的重要性。
举个例子哈:在「代码随想录」刷题小分队微信群里,一些录友可能代码通过不了,会把代码抛到讨论群里问:我这里代码都已经和题解一模一样了,为什么通过不了呢?
发出这样的问题之前,其实可以自己先思考这三个问题:
- 这道题目我举例推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。
总结
这一篇是动态规划的整体概述,讲解了什么是动态规划,动态规划的解题步骤,以及如何debug。
动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列中都会使用到的一些理论基础。
2.斐波那契数
力扣题目链接(opens new window)
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
示例 1:
- 输入:2
- 输出:1
- 解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
- 输入:3
- 输出:2
- 解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
- 输入:4
- 输出:3
- 解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
- 0 <= n <= 30
思路
斐波那契数列大家应该非常熟悉不过了,非常适合作为动规第一道题目来练练手。
因为这道题目比较简单,可能一些同学并不需要做什么分析,直接顺手一写就过了。
但「代码随想录」的风格是:简单题目是用来加深对解题方法论的理解的。
通过这道题目让大家可以初步认识到,按照动规五部曲是如何解题的。
对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。
所以我总结的动规五部曲,是要用来贯穿整个动态规划系列的,就像之前讲过二叉树系列的递归三部曲 (opens new window),回溯法系列的回溯三部曲 (opens new window)一样。后面慢慢大家就会体会到,动规五部曲方法的重要性。
动态规划
动规五部曲:
这里我们要用一个一维dp数组来保存递归的结果
- 确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
- 确定递推公式
为什么这是一道非常简单的入门题目呢?
因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
- dp数组如何初始化
- 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
- 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
递归解法
本题还可以使用递归解法来做
这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:通过一道面试题目,讲一讲递归算法的时间复杂度!(opens new window)
总结
斐波那契数列这道题目是非常基础的题目,我在后面的动态规划的讲解中将会多次提到斐波那契数列!
这里我严格按照关于动态规划,你该了解这些! (opens new window)中的动规五部曲来分析了这道题目,一些分析步骤可能同学感觉没有必要搞的这么复杂,代码其实上来就可以撸出来。
public class Fibonacci_number {public int fib1(int n) {if (n < 2) return n;//如果 n 小于 2,直接返回 n,因为斐波那契数列的前两个数分别是 0 和 1。int a = 0, b = 1, c = 0;//a、b 和 c,分别代表当前数列的前两个数和它们的和。初始时,a = 0,b = 1,c = 0。for (int i = 1; i < n; i++) {//从 1 迭代到 n - 1,每次迭代计算下一个斐波那契数,更新 a 和 b 的值。c = a + b;a = b;b = c;}return c;//最终返回 c,即第 n 个斐波那契数。}}
时间复杂度:O(n)
空间复杂度:O(1)
public class Fibonacci_number {public int fib2(int n) {if (n <= 1) return n;//如果 n 小于或等于 1,直接返回 n,因为斐波那契数列的前两个数分别是 0 和 1。int[] dp = new int[n + 1];//创建一个长度为 n + 1 的数组 dp,为了存储从 F(0) 到 F(n) 的所有斐波那契数。用于存储斐波那契数列的每个数。dp[0] = 0;dp[1] = 1;for (int index = 2; index <= n; index++){//从 2 迭代到 n,每次迭代计算下一个斐波那契数,并存储在 dp[index] 中。dp[index] = dp[index - 1] + dp[index - 2];}return dp[n];}
}
时间复杂度:O(n)
空间复杂度:O(n)
3.爬楼梯
力扣题目链接(opens new window)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
- 输入: 2
- 输出: 2
- 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
- 输入: 3
- 输出: 3
- 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
思路
爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。
那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。
所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。
我们来分析一下,动规五部曲:
定义一个一维数组来记录不同楼层的状态
- 确定dp数组以及下标的含义
dp[i]: 爬到第i层楼梯,有dp[i]种方法
- 确定递推公式
如何可以推出dp[i]呢?
从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。
在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。
这体现出确定dp数组以及下标的含义的重要性!
- dp数组如何初始化
再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。
那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。
例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。
但总有点牵强的成分。
那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.
其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1。
从dp数组定义的角度上来说,dp[0] = 0 也能说得通。
需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。
所以本题其实就不应该讨论dp[0]的初始化!
我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。
所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。
- 确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
- 举例推导dp数组
举例当n为5的时候,dp table(dp数组)应该是这样的
如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。
此时大家应该发现了,这不就是斐波那契数列么!
唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义!
拓展
这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。
这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,大家可以去卡码网去做一下 57. 爬楼梯(opens new window)
所以后续我在讲解背包问题的时候,今天这道题还会从背包问题的角度上来再讲一遍。 如果想提前看一下,可以看这篇:70.爬楼梯完全背包版本
一个绝佳的大厂面试题,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。
然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。
这一连套问下来,候选人算法能力如何,面试官心里就有数了。
其实大厂面试最喜欢的问题就是这种简单题,然后慢慢变化,在小细节上考察候选人。
总结
这道题目和动态规划:斐波那契数 (opens new window)题目基本是一样的,但是会发现本题相比动态规划:斐波那契数 (opens new window)难多了,为什么呢?
关键是 动态规划:斐波那契数 (opens new window)题目描述就已经把动规五部曲里的递归公式和如何初始化都给出来了,剩下几部曲也自然而然的推出来了。
而本题,就需要逐个分析了,大家现在应该初步感受出关于动态规划,你该了解这些! (opens new window)里给出的动规五部曲了。
简单题是用来掌握方法论的,例如昨天斐波那契的题目够简单了吧,但昨天和今天可以使用一套方法分析出来的,这就是方法论!
public class Climbing_Stairs {public int climbStairs1(int n) {int[] dp = new int[n + 1];//创建一个长度为 n + 1 的数组 dp,用于存储从第0个台阶到第 n 个台阶的爬法数量。dp[0] = 1;//表示在地面上(第0个台阶)只有1种方式(啥都不做)。dp[1] = 1;//表示到达第1个台阶只有1种方式(即向上爬1个台阶)。for (int i = 2; i <= n; i++) {//从2迭代到 n,每次迭代计算到达当前台阶的不同方法数,并存储在 dp[i] 中。到达第 i 个台阶的方法数等于到达第 i-1 个台阶的方法数加上到达第 i-2 个台阶的方法数,因为可以从第 i-1 个台阶走1步上来,或者从第 i-2 个台阶走2步上来。dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];}}
时间复杂度:O(n)
空间复杂度:O(n)
public class Climbing_Stairs {public int climbStairs2(int n) {if(n <= 2) return n;//如果 n 小于或等于2,直接返回 n,因为对于前两个台阶,爬法数量就是台阶数。int a = 1, b = 2, sum = 0;// a、b 和 sum,分别代表当前数列的前两个数和它们的和。初始时,a = 1(第1个台阶的爬法数),b = 2(第2个台阶的爬法数),sum = 0。for(int i = 3; i <= n; i++){//从3迭代到 n,每次迭代计算下一个台阶的爬法数,并更新 a 和 b 的值。sum 保存当前台阶的爬法数,a 更新为 b 的值,b 更新为 sum 的值。sum = a + b;a = b;b = sum;}return b;}
}
时间复杂度:O(n)
空间复杂度:O(1)
4.使用最小花费爬楼梯
力扣题目链接(opens new window)
旧题目描述:
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
- 输入:cost = [10, 15, 20]
- 输出:15
- 解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
示例 2:
- 输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
- 输出:6
- 解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
提示:
- cost 的长度范围是 [2, 1000]。
- cost[i] 将会是一个整型数据,范围为 [0, 999] 。
本题之前的题目描述是很模糊的,看不出来,第一步需要花费体力值,最后一步不用花费,还是说 第一步不花费体力值,最后一步花费。
后来力扣改了题目描述,新题目描述:
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
思路
(在力扣修改了题目描述下,我又重新修改了题解)
修改之后的题意就比较明确了,题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。
- 确定dp数组以及下标的含义
使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
对于dp数组的定义,大家一定要清晰!
- 确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?
一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
- dp数组如何初始化
看一下递归公式,dp[i]由dp[i - 1],dp[i - 2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
那么 dp[0] 应该是多少呢? 根据dp数组的定义,到达第0台阶所花费的最小体力为dp[0],那么有同学可能想,那dp[0] 应该是 cost[0],例如 cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 的话,dp[0] 就是 cost[0] 应该是1。
这里就要说明本题力扣为什么改题意,而且修改题意之后 就清晰很多的原因了。
新题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。
所以初始化 dp[0] = 0,dp[1] = 0;
- 确定遍历顺序
最后一步,递归公式有了,初始化有了,如何遍历呢?
本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。
因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 例如:01背包,都知道两个for循环,一个for遍历物品嵌套一个for遍历背包容量,那么为什么不是一个for遍历背包容量嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒序呢?
这些都与遍历顺序息息相关。当然背包问题后续「代码随想录」都会重点讲解的!
- 举例推导dp数组
拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:
如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。
总结
大家可以发现这道题目相对于 昨天的动态规划:爬楼梯 (opens new window)又难了一点,但整体思路是一样的。
从动态规划:斐波那契数 (opens new window)到 动态规划:爬楼梯 (opens new window)再到今天这道题目,录友们感受到循序渐进的梯度了嘛。
第一步不支付费用
public class Climbing_Stairs_with_Minimum_Cost {public int minCostClimbingStairs1(int[] cost) {int len = cost.length;int[] dp = new int[len + 1];//创建一个长度为 cost.length + 1 的数组 dp,用于存储从第0个台阶到第 len 个台阶的最小花费。dp[0] = 0;//从地面(第0个台阶)或第1个台阶开始爬不需要花费。dp[1] = 0;for (int i = 2; i <= len; i++) {//从2迭代到 len,每次迭代计算到达当前台阶的最小花费,并存储在 dp[i] 中。到达第 i 个台阶的最小花费等于到达第 i-1 个台阶的最小花费加上当前台阶的花费,或者到达第 i-2 个台阶的最小花费加上当前台阶的花费,取两者的最小值。dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[len];//最终返回 dp[len],即到达第 len 个台阶的最小花费。}
}
时间复杂度:O(n)
空间复杂度:O(n)
第一步支付费用
public class Climbing_Stairs_with_Minimum_Cost {public int minCostClimbingStairs2(int[] cost) {int[] dp = new int[cost.length];//创建一个长度为 cost.length 的数组 dp,用于存储从第0个台阶到第 cost.length - 1 个台阶的最小花费。dp[0] = cost[0];//初始化 dp[0] 和 dp[1] 为 cost[0] 和 cost[1],因为到达第1个台阶和第2个台阶的最小花费就是它们各自的花费。dp[1] = cost[1];for (int i = 2; i < cost.length; i++) {//从2迭代到 cost.length - 1,每次迭代计算到达当前台阶的最小花费,并存储在 dp[i] 中。到达第 i 个台阶的最小花费等于到达第 i-1 个台阶的最小花费加上当前台阶的花费,或者到达第 i-2 个台阶的最小花费加上当前台阶的花费,取两者的最小值。dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];}return Math.min(dp[cost.length - 1], dp[cost.length - 2]);//最终返回 dp[cost.length - 1] 和 dp[cost.length - 2] 的最小值,因为到达顶部可以是从倒数第二步或倒数第三步爬上来的。}}
时间复杂度:O(n)
空间复杂度:O(n)
状态压缩,使用三个变量来来代替数组
public class Climbing_Stairs_with_Minimum_Cost {public int minCostClimbingStairs3(int[] cost) {int beforeTwoCost = 0, beforeOneCost = 0, currentCost = 0;//用三个变量 beforeTwoCost、beforeOneCost 和 currentCost 来存储到达前两个台阶和当前台阶的最小花费。for (int i = 2; i <= cost.length; i ++) {//从2迭代到 cost.length,每次迭代计算到达当前台阶的最小花费,并更新 beforeTwoCost、beforeOneCost 和 currentCost 的值。currentCost 等于到达第 i-1 个台阶的最小花费加上当前台阶的花费,或者到达第 i-2 个台阶的最小花费加上当前台阶的花费,取两者的最小值。然后,beforeTwoCost 更新为 beforeOneCost 的值,beforeOneCost 更新为 currentCost 的值。currentCost = Math.min(beforeOneCost + cost[i - 1], beforeTwoCost + cost[i - 2]);beforeTwoCost = beforeOneCost;beforeOneCost = currentCost;}return currentCost;//最终返回 currentCost,即到达第 cost.length 个台阶的最小花费。}
}
时间复杂度:O(n)
空间复杂度:O(1)
相关文章:

代码随想录32 动态规划理论基础,509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯。
1.动态规划理论基础 动态规划刷题大纲 什么是动态规划 动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的…...

记录一个Flutter 3.24单元测试点击事件bug
哈喽,我是老刘 这两天发现一个Flutter 3.24版本的单元测试的一个小bug,提醒大家注意一下。 老刘自己写代码十多年了,写Flutter也6年多了,没想到前两天在一个小小的BottomNavigationBar 组件上翻了车。 给大家分享一下事件的经过。…...
使用Python将 word文档转pdf文档
第一步:我们需要导入支持包 >pip install pywin32 如果下载速度比较慢的话,可以考虑使用国内镜像源。 第二步:我们需要导入文件,这里采用 input,用户填入路径后,直接获取路径下的word文档,实现批量转换…...

基于C#+SQLite开发数据库应用的示例
SQLite数据库,小巧但功能强大;并且是基于文件型的数据库,驱动库就是一个dll文件,有些开发工具 甚至不需要带这个dll,比如用Delphi开发,用一些三方组件;数据库也是一个文件,虽然是个文…...

Vue基本语法
Options API 选项式/配置式api 需要在script中的export default一个对象对象中可以包含data、method、components等keydata是数据,数据必须是一个方法(如果是对象,会导致多组件的时候,数据互相影响,因为对象赋值后&…...
芯片发展史
芯片的发展史可分为几个重要的阶段,从早期的真空管到现代的集成电路,反映了技术进步和创新的历程: 1. 真空管时代 (1904 - 1950年代) 真空管是20世纪初的电子元件,用于放大信号和开关,广泛应用在早期的收音机、电视机…...

我的知识图谱和Neo4j数据库的使用
知识图谱概述 知识图谱的含义 RDF与RDFS RDF(Resource Description Framework,资源描述框架)和RDFS(RDF Schema,RDF模式)是构建知识图谱的基础技术之一。它们提供了一种标准的方式来表示信息,…...

ASP.NET CORE API 解决跨域问题
环境 vs2022 .net 8 创建ASP.net Core API项目 配置跨域 编写ApiController 启动项目 得到服务器运行的 地址 在Hbuiler中创建web项目,编写代码 【运行】-【运行到浏览器】-选择一个浏览器,查看结果 正常显示 问题 如果允许所有源访问,有安全风险方…...
sram测试注意讨论
常规测试首先是mbist测试,原理不用多说,自己看,主要是注意点和考虑点: 1、明确测试用的到func_clk的频率的大小,根据经验值一般大于800M的时钟需要特别考虑Timing的问题:由于pr摆放的位置原因,…...
Mybatis 支持延迟加载的详细内容
延迟加载的概念深入 延迟加载是一种在处理复杂对象关系时非常有用的策略。在企业级应用开发中,数据库中的表之间往往存在着各种关联关系,如一对多(一个用户有多个订单)、多对多(一个学生可以选多门课程,一门…...
word文档使用技巧笔记
中文和数字断开到第二行 word一串数字断开_百度知道 下划线对齐 word下划线怎么固定长度一致-百度经验...

使用docker-compose部署搜索引擎ElasticSearch6.8.10
背景 Elasticsearch 是一个开源的分布式搜索和分析引擎,基于 Apache Lucene 构建。它被广泛用于实时数据搜索、日志分析、全文检索等应用场景。 Elasticsearch 支持高效的全文搜索,并提供了强大的聚合功能,可以处理大规模的数据集并进行快速…...

bugku-web-login2
不知道为啥用bp始终登不上hackbar可以 随便输入个账号密码bp抓包,发现个小tip是base64加密的解密 $sql"SELECT username,password FROM admin WHERE username".$username.""; if (!empty($row) && $row[password]md5($password)){ } …...

【 AI技术赋能有限元分析与材料科学应用实践】Neo-Hookean 材料与深度学习结合的有限元分析
Neo-Hookean 材料模型是用于描述非线性弹性材料(如软组织和橡胶等)的经典模型,特别适用于大变形问题。其基本思想是通过应变能密度函数来描述材料的弹性行为。在该模型中,材料的应力-应变关系不仅依赖于应变能,还通过变…...
StarRocks关于ConcurrentModificationException 问题的解决
背景 本文基于 StarRocks 3.1.7 目前在基于Starrocks做一些数据分析的操作(主要是做一些简单的查询),同事遇到了一些并发的问题: ontent:2024-11-27 07:04:34,048 WARN (starrocks-mysql-nio-pool-214933|3593819) [StmtExecutor.execute():643] execute Exceptio…...

网络安全防护指南:筑牢网络安全防线(5/10)
一、网络安全的基本概念 (一)网络的定义 网络是指由计算机或者其他信息终端及相关设备组成的按照一定的规则和程序对信息收集、存储、传输、交换、处理的系统。在当今数字化时代,网络已经成为人们生活和工作中不可或缺的一部分。它连接了世…...

替代FTP最佳跨网文件传输解决方案——FileLink
在传统的企业文件传输中,FTP(文件传输协议)曾因其便捷性和高效性被广泛应用。然而,其固有的安全漏洞、对大文件传输支持的局限性、易受网络攻击等问题,已逐渐暴露出FTP在现代企业环境下的不足。针对这一问题࿰…...
Cesium在vue2中的引入和注意事项
在Vue2中,可以使用npm包管理工具来安装Cesium,并通过import语句将其引入到项目中。下面是在Vue2中引入Cesium的步骤和注意事项: 步骤: 首先,打开终端并进入Vue项目的根目录。 运行以下命令来安装Cesium: …...

CentOS 9 配置静态IP
文章目录 1_问题原因2_nmcli 配置静态IP3_使用配置文件固定IP4_重启后存在的问题5_nmcli 补充 1_问题原因 CentOS 7 于 2014年6月发布,基于 RHEL 7,并在 2024年6月30日 结束维护。 CentOS 9 作为目前的最新版本,今天闲来闲来无事下载下来后…...
深入解析 Webhook:从原理到实践的全面指南
1. 引言 1.1 什么是 Webhook? Webhook 是一种基于 HTTP 回调的轻量级通信机制,它允许一个系统实时向另一个系统发送数据。当特定事件发生时,Webhook 会主动向指定的 URL 发送 HTTP 请求,通常携带事件相关的数据。这种被动接收通…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...