【LeetCode动态规划#01】动规入门:求斐波那契数 + 爬楼梯 + 最小代价爬楼梯(熟悉解题方法论)

斐波那契数

力扣题目链接(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

思路

本来这题应该是用递归写的,但作为DP的入门题也合适

先说明一下,DP五部曲:

  • 确定DP数组以及数组的下标定义
  • 确定递推公式
  • 确定DP数组如何初始化
  • 确定遍历顺序
  • 打印DP数组(主要用于debug)
五部曲分析

那下面就用这题来做一个示范

1、确定DP数组以及数组的下标定义

做动规题都需要先明确dp数组dp[i]的定义

在本题中,dp[i]应该指的是:第i个斐波那契数的数值是dp[i]

2、确定递推公式

因为刚刚入门,就目前我的理解,所谓的递推公式就是题目中的某种解决问题的思路的抽象形式

像这里,题目直接就给了F(n) = F(n - 1) + F(n - 2)

这个公式就是用来求斐波那契数的,那么这个公式也就是要找的递推公式(直接给出了所以本题简单)

结合本题dp[i]的定义可以得到最终需要的递推公式:dp[i] = dp[i - 1] + dp[i - 2]

3、确定DP数组初始化方式

题目也给了:F(0) = 0,F(1) = 1,所以初始化方式如下:

dp[0] = 0;
dp[1] = 1;

4、确定遍历顺序

因为dp[i] = dp[i - 1] + dp[i - 2],先有的dp[i - 1]和dp[i - 2]才能求dp[i]

故遍历顺序应该是从前向后

5、打印DP数组

这一步的意思就是,根据我们推断的递推公式,将dp[i]的值自行计算出来看看对不对

代码

class Solution {
public:
    int fib(int n) {
        if(n <= 1) return n;//如果n小于等于1,即直接得到第1个斐波那契数(0 + 1),因此直接返回n即可。     
        vector<int> dp(n + 1);//创建dp数组
        dp[0] = 0;//初始化dp数组
        dp[1] = 1;

        for(int i = 2; i <= n; ++i){//遍历
            dp[i] = dp[i - 1] + dp[i - 2];   
        }
        return dp[n];//返回dp数组中n处的值,即第n个斐波那契数
    }
};

爬梯子

力扣题目链接(opens new window)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

  • 输入: 2
  • 输出: 2
  • 解释: 有两种方法可以爬到楼顶。
    • 1 阶 + 1 阶
    • 2 阶

示例 2:

  • 输入: 3
  • 输出: 3
  • 解释: 有三种方法可以爬到楼顶。
    • 1 阶 + 1 阶 + 1 阶
    • 1 阶 + 2 阶
    • 2 阶 + 1 阶

思路

先看看题目描述,要求的是爬到楼顶有多少种方法

再看一下示例2,

爬到第一层楼梯有1种方法;

爬到第二层楼梯有2种方法;(一阶一阶爬、一次爬两阶)

那么爬到第三层楼梯就有3种方法;(一阶一阶爬、一阶+二阶、二阶+一阶)

实际上到第三层楼梯的方法数可以通过到第一层和到第二层的方法数推出

那么就可以用dp

五部曲

还是五步走

1、确定dp数组的含义

dp[i]:爬到第i层楼梯有dp[i]种方法

2、确定递推公式

如何推出递推公式?要结合对于dp[i]的定义

1阶   1种  dp[i-2],上i-2层楼梯,有dp[i - 2]种方法(现在是i-2阶,再往上走1阶到i-1阶)
2阶   2种  dp[i-1],上i-1层楼梯,有dp[i - 1]种方法(现在是i-1阶,再往上走1阶到i阶)
3阶   3种  dp[i],上i层楼梯,有dp[i]种方法

好,到第i层楼梯(也就是第3层),按dp数组定义来是有dp[i]种方法

现在往回看,在i-2层时,我们可以选择一次爬两阶,然后上到第i层;同理在i-1层时,我们也可以选择一次爬一阶,然后上到第i层;

这说明,到达第i层楼梯的dp[i]种方法中包含着爬上i-2层和i-1层时的方法

由此可以总结出到达第i层楼梯的递推公式:dp[i] = dp[i - 2] + dp[i - 1]

3、确定dp数组的初始化方式

之前在 斐波那契数 中,我们讨论初始化时,是有对dp[0]进行初始化的

但是这里可以不用讨论dp[0],下面来说具体原因

在确定dp数组的初始化方式时,我们仍然要遵循第一步中给dp数组下的定义,即dp[i]:爬到第i层楼梯有dp[i]种方法

根据此定义来解释dp[0]就有点问题,爬到第0层楼有dp[0]种方法?

都0层楼了,相当于不用走直接在终点了,这样就可能会有多种解释

最好的办法就是不讨论dp[0]的情况,而且题目中说了n是一个正整数,根本就没说n有为0的情况。

那么根据dp数组的定义,我们可以得到以下初始化:

dp[1] = 1;
dp[2] = 2;

显然这是无争议,也是符合dp数组定义的

4、确定遍历顺序

分析到这里了,你肯定发现这题和斐波那契数其实是一样的

第i个数(阶梯)需要依靠第i-1和i-2个数(阶梯)确定

自然的,本题的遍历顺序也需要是从前向后

代码

因为我们不讨论dp[0],所以应该从dp[3]开始累加

步骤:

1、处理n小于等于1的情况

2、创建dp数组,并初始化

3、按照设定的顺序开始遍历累加dp数组,最后返回dp[n]

class Solution {
public:
    int climbStairs(int n) {
        if(n <= 1) return n;//如果n小于等于1,那么只有一种爬楼梯的方法,即直接到达目标楼层,因此直接返回n即可。
        vector<int> dp(n + 1);//创建dp数组

        dp[1] = 1;//初始化dp数组
        dp[2] = 2;
        for(int i = 3; i <= n; ++i){// 注意i是从3开始的
            dp[i] = dp[i - 2] + dp[i - 1];
        }
        return dp[n];//返回dp数组中n处的值,即爬到第i层楼梯有dp[i]种方法
    }
};

在这段代码中,如果n小于等于1,那么只有一种爬楼梯的方法,即直接到达目标楼层,因此直接返回n即可。

如果n大于1,那么创建一个大小为n+1的dp数组来记录每个楼梯台阶的爬楼梯方法数。然后初始化dp数组的前两个元素为1和2,因为到达第一层楼梯只有一种方法,到达第二层楼梯有两种方法。接着通过循环,从第三个元素开始依次计算每个楼梯的爬楼梯方法数,即dp[i] = dp[i - 2] + dp[i - 1],最后返回dp数组中n处的值,即到达第n层楼梯的爬楼梯方法数。

使用最小花费爬楼梯

力扣题目链接(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] 。

思路

由题意,我们每向上爬一格阶梯(可以选择从第0阶或者第1阶开始,并且每次可以跳一格或两格),需要花费当前阶梯(也就是起跳台阶)规定的体力

例如示例1中,我们第一次爬一格到第二级阶梯,此处阶梯价格为15,那么目前花费就是15,然后再爬一格到达顶层,结束

总共花费就是15

从这个过程中可以发现:

​ 1、当前"站"在某个阶梯上是不扣体力的(例如最开始站在第1阶(15)上,不会扣15)

​ 2、只有当爬到某一台阶后才会扣取出发(起跳)台阶的标价(选择从第一阶出发,从第1阶跳两阶到第3阶,扣掉第一阶上的标价15)

​ 3、到达顶层之后,顶层不扣体力(还是拿示例1来说,这里的第2阶(20)不是顶层,顶层应该是第3阶)

题意差不多清楚之后,可以来看五部曲了

五步走

1、确定dp数组的定义

其实题目中就给了

dp[i]:到达第i个阶梯的最小体力花费dp[i]

2、确定递推公式

dp[i]  ↑ ↑
dp[i-1]↑ ↑//dp[i]可以由dp[i-1]跳了一步得到
dp[i-2]↑ //dp[i]也可以由dp[i-2]跳了两步得到

而跳跃之后需要花费起跳台阶上的对应体力消耗,所以可以得到以下式子

dp[i]  
dp[i-1]+cost[i-1]
dp[i-2]+cost[i-2]

通过上面的式子可以得知,从i-1跳可以得到dp[i],从i-2跳也可以得到dp[i],取哪个呢?

因为题意让我们找的是最小体力花费,并且对于dp数组的定义也是如此

因此我们要取这两者中最小的一个,即:

dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);

这便是递推公式

3、确定dp数组的初始值

如何初始化dp数组呢?这里还需要分析

根据递推公式可知

dp[2]可以由dp[1]和dp[0]求得

dp[3]可以由dp[2]和dp[1]求得

...以此类推

可见,dp[1]和dp[0]是求数组中后续数值的基础,因此需要对dp[1]和dp[0]进行初始化

怎么初始化呢?

根据题意,我们可以从0阶或者1阶开始

又由前面的分析可知,我们"站"在某个位置是不需要花费体力的

那么不论从0阶还是1阶出发,只要还没开始跳,花费都是0(不管0、1阶处台阶标价为多少)

因此可以将dp[1]和dp[0]都初始化为0

4、确定遍历顺序

从递推公式看,后面的元素时依靠前面的元素来推导出来的,因此遍历顺序还是从前往后

代码

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        //创建dp数组
        vector<int> dp(cost.size() + 1);
        //初始化dp数组
        dp[0] = 0;
        dp[1] = 0;
        //遍历
        for(int i = 2; i <= cost.size(); ++i){//从2开始,因为0、1都是可选的出发位置
            dp[i] = min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
        }
        return dp[cost.size()];//返回爬到第cost.size()个台阶时的体力消耗
    }
};
posted @ 2023-03-24 13:20  dayceng  阅读(63)  评论(0编辑  收藏  举报