算法学习|Day38 动态规划|Leetcode 509. 斐波那契数、70. 爬楼梯 、746. 使用最小花费爬楼梯

1. 动态理论

链接:动态规划理论

动态规划五部曲:

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

2. 509. 斐波那契数

2.1 思路

斐波那契数列大家应该非常熟悉不过了,非常适合作为动规第一道题目来练练手;因为这道题目比较简单,可能一些同学并不需要做什么分析,直接顺手一写就过了;但「代码随想录」的风格是:简单题目是用来加深对解题方法论的理解的。

通过这道题目让大家可以初步认识到,按照动规五部曲是如何解题的。对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。

动态规划五部曲

  1. 确定dp数组及下标的含义

    这里我们要用一个一维dp数组来保存递归的结果,dp[i]的定义为:第i个数的斐波那契数值是dp[i];

  2. 确定递推公式

    为什么这是一道非常简单的入门题目呢?因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];

  3. dp数组如何初始化

    题目中把如何初始化也直接给我们了,如下:dp[0] = 0;dp[1] = 1;

  4. 确定遍历顺序

    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的;

  5. 举例推导dp数组

    按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55;如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的;

2.2 代码实现

class Solution {
    public int fib(int n) {
        if(n == 0) return 0;
        if(n == 1) return 1;
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}
  • 时间复杂度:O(n),因为我们需要遍历数组。
  • 空间复杂度:O(n),因为我们需要考虑存储 dp 数组的空间。

优化版本:

class Solution {
    public int fib(int n) {
        if (n<2) {
            return n;
        }
        int a=0;//f(0)
        int b=1;//f(1)
        int c=0;//和
        for (int i = 2; i <=n; i++) {
            c=a+b;//开始f(2)=0+1;
            a=b;
            b=c;
        }
        return c;
    }

在这个优化版本中,我们只使用了三个整型变量 a, b, c 来动态计算斐波那契数列,而不需要一个数组了。在循环迭代中,我们先计算 c 的值(即 dp[i]),然后更新 a 和 b 的值(分别对应 dp[i-2] 和 dp[i-1])。最后,当 i = n 时,我们返回 c 即可。

  • 时间复杂度:O(n),因为我们还是需要遍历整个斐波那契数列。

  • 空间复杂度:O(1),因为我们只需要使用几个基本变量来存储 dp[i]、dp[i-1] 和 dp[i-2] 的值。

3. 70.爬楼梯

3.1 思路

  1. 确定dp数组以及下标的含义
    dp[i]: 爬到第i层楼梯,有dp[i]种方法

  2. 确定递推公式
    如果可以推出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数组以及下标的含义的重要性!

  1. dp数组初始化
    在回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]中方法。

那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但是基本上都是奔着答案去解释的;

  • dp[0] = 1

例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶,但总有点牵强的成分。

那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.

其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1;

  • dp[0] = 0

从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]的定义;

  1. 确定遍历顺序
    从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的;

  2. 举例推导dp数组
    举例当n为5的时候,dp table(dp数组)应该是这样的

3.2 代码实现

class Solution {
    public int climbStairs(int n) {
        if (n<3) return n;
        //定义数组
        int[] dp=new int[n+1];
        //初始化
        dp[1]=1;
        dp[2]=2;
        //遍历
        for (int i = 3; i <=n; i++) {
            //递归
            dp[i]=dp[i-2]+dp[i-1];
        }
        return dp[n];
    }
  • 时间复杂度:O(n),因为我们需要遍历整个楼梯阶数至少一次。
  • 空间复杂度:O(n),因为我们需要考虑存储 dp 数组的空间。

优化版本

class Solution {
    public int climbStairs(int n) {
        if (n<3) return n;

        int a=1,b=2,c=0;
        for (int i = 3; i <=n; i++) {
            c=a+b;
            a=b;
            b=c;
        }
        return c;
    }
}
  • 时间复杂度:O(n),因为我们需要遍历整个楼梯阶数至少一次。
  • 空间复杂度:O(1)

3.3 总结

  • 此题类似斐波那契数
  • 遍历初始位置跟if语句中的n小于的那个数相匹配

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

4.1 思路

  1. 确定dp数组以及下标的含义
    使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了;dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i];(注意这里认为是第一步一定是要花费)对于dp数组的定义,大家一定要清晰!

  2. 确定递推公式
    可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2],从i-1和i-2 位置调到i位置,还需要另外花费cost[i-1]和cost[i-2]的值,这两条路一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])

  3. dp数组如何初始化
    根据dp数组的定义,只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出;因为站在i=0 或者i=1的位置都是不需要花费的,所以dp[0] = 0;dp[1] = 0;

  4. 确定遍历顺序

    本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。因为是模拟台阶,而且dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了;

  5. 举例推导dp数组
    拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:

    dp[i] = [0,0,1,2,2,3,3,4,5,5,6],比cost长一位,取最后一位即为结果;如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的;

4.2 代码实现

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        //定义dp数组
        int[] dp=new int[cost.length+1];
        dp[0]=0;
        dp[1]=0;

        for (int i = 2; i <=cost.length; i++) {
            dp[i]=Math.min(dp[i-2]+cost[i-2],dp[i-1]+cost[i-1]);
        }
        return dp[cost.length];
    }
}

解释:

int[] dp=new int[cost.length+1];

题目中有说到从下标为 0 或下标为 1 的台阶开始爬楼梯,对应的费用为 0,因此 dp 数组的长度应该比原数组长度多 1。这样,dp[i] 就对应了原数组中 cost[i-1] 的花费,dp[0] 和 dp[1] 都设置为 0,表示从第 0 个台阶和第 1 个台阶出发都不需要支付费用。

因此,可以使用一个长度为 len+1 的 dp 数组,其中 dp[0] 和 dp[1] 都等于 0,dp[i] 表示到达第 i 个阶梯的最小花费。最后返回 dp[len] 即可。

在编写代码时,可以注意到 0 号元素没有使用,可以不用开数组的情况下直接使用两个变量表示当前和前一阶台阶的最小花费。

优化版

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int dp1 = 0, dp2 = 0;
        for (int i = 2; i <= cost.length; i++) {
            int dp = Math.min(dp1 + cost[i - 2], dp2 + cost[i - 1]);
            dp1 = dp2;
            dp2 = dp;
        }
        return dp2;
    }
}

只需要定义两个变量 dp1 和 dp2,用来保存上一次和当前的最小花费,对于当前的阶梯 i,使用 dp = Math.min(dp1 + cost[i - 2], dp2 + cost[i - 1]) 计算到达第 i 个阶梯的最小花费,然后将 dp2 的值赋值为 dp1,dp1 的值赋值为 dp,即 dp1 = dp2,dp2 = dp。

最后,返回 dp2 即为最小跨越花费。

  • 时间复杂度:O(n),需要遍历整个花费数组。
  • 空间复杂度:O(1),只需要常量空间保存两个变量,因此空间复杂度为 O(1)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值