DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯

文章目录

    • 509.斐波那契数列
      • 思路:动规五步
        • 确定dp数组和数组下标含义
        • 递推公式
        • DP数组初始化
        • 遍历顺序
        • 打印DP数组
      • 完整版
        • debug测试
      • 空间复杂度优化版
        • 优化思路
    • 70.爬楼梯
      • 思路
        • DP数组的含义以及下标含义
        • 递推公式
        • DP数组初始化
        • 遍历顺序
        • 打印DP数组
      • 完整版
        • debug测试
      • 空间复杂度优化写法
    • 746.使用最小花费爬楼梯
      • 思路
        • DP数组含义
        • 递推公式
        • DP数组初始化
        • 遍历顺序
          • 遍历顺序补充
        • 打印DP数组
      • 完整版
      • 空间复杂度优化写法

509.斐波那契数列

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30

思路:动规五步

确定dp数组和数组下标含义

DP题目都需要定义一维或者二维的状态转移数组,通常是叫dp。

本题中,dp[i]表示第i个斐波那契数的数值为dp[i]

递推公式

本题是比较简单的DP题目,就是因为题目描述中已经把递推公式告诉我们了。

递推公式:dp[i]=dp[i-1]+dp[i-2]

DP数组初始化

题目描述已经说了,dp[0]=1

遍历顺序

因为dp[i]是由dp[i-1]和dp[i-2]得到的,因此需要从前往后遍历,才能保证每次dp[i]能够考虑到前面的两个元素。

打印DP数组

这一步主要用于debug,打印出来看看和想象的是否一样

完整版

class Solution {
public:
    int fib(int n) {
        if(n==0) 
            return 0;
        if(n==1) 
            return 1;
        //建立dp数组
        vector<int>dp(n+1);
        //dp数组初始化,初始化依赖于递推公式
        //注意这里初始化需要放到if特殊情况后面,因为如果n是0,就不存在dp[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)
debug测试

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第1张图片
这段代码的问题出在没有处理 n 为 0 或 1 的情况。如果 n 为 0,那么 dp[1] 就不存在,这时试图访问 dp[1] 会导致溢出

dp[0]=0;dp[1]=1;if特殊情况需要放最前面,因为如果n是0,就不存在dp[1]

修改加上if条件之后通过。

空间复杂度优化版

  • 实际上,我们只需要维护两个数值就可以了,不需要记录整个序列。
class Solution {
public:
    int fib(int N) {
        if (N <= 1) return N;
        int dp[2];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= N; i++) {
            int sum = dp[0] + dp[1];
            dp[0] = dp[1];
            dp[1] = sum;
        }
        return dp[1];
    }
};
优化思路

斐波那契数列的定义:F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2) 对于所有 n >= 2。这意味着,要计算第 n 个斐波那契数,只需要知道前两个斐波那契数,即 F(n-1) 和 F(n-2)

优化版本的斐波那契数列计算利用了这个性质。在循环开始时,dp[0] 和 dp[1] 分别存储 F(n-2) 和 F(n-1)。然后,我们计算新的斐波那契数 F(n) = dp[0] + dp[1],并更新 dp[0] 和 dp[1],以备下一个循环使用。所以,我们只需要两个变量就可以计算出斐波那契数列的下一个值,而不必维护整个数列。

这样的优化实际上是一个空间优化,称为 “滚动数组” 或者 “滑动窗口” 的策略。其基本思想是只保存当前阶段需要的数据,淘汰过去不再需要的数据,避免存储不必要的信息,从而降低空间复杂度。

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

70.爬楼梯

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

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

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1+ 12. 2

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。

1. 1+ 1+ 12. 1+ 23. 2+ 1

提示:

  • 1 <= n <= 45

思路

一共n阶台阶,1阶:1步,2阶:2种(2或者1+1),3阶:3种(2+1或1+2或1+1+1)4阶:5种。

我们可以发现,3阶,只能从1阶和2阶迈上来,实际上就是1阶的1种方法加上2阶的2种方法

4阶,只能从2阶和3阶迈上来,因此登上4阶的方法数就是登上2阶的方法数+登上3阶的方法数,2+3=5种。

我们此时就可以发现递推关系,也就是当前阶梯的状态,依赖于他的前两个阶梯的状态。(一次性最多迈两步)

也就是说,因为每次只能爬 1 级或 2级,所以f(x)的数值只能从f(x-1)和f(x-2)转移过来。而这里要统计方案总数,我们就需要对这两项的贡献求和

DP数组的含义以及下标含义

dp[i]:达到第i阶,有dp[i]种方法。

后面的推导都是基于含义

递推公式

dp[i]=dp[i-1]+dp[i-2],其中dp[i-1]表示达到第i-1阶有多少种方法,dp[i-2]同理

DP数组初始化

首先因为题目描述达到第一阶有1种方法第二阶有2种,所以dp[1]=1,dp[2]=2.

dp[0]的含义是,达到第0阶需要多少种方法。但是本题中,dp[0]是没有意义的!因为题目给出的数据范围,n是一个>=1的正整数,因此我们完全不需要考虑dp[0]的情况,也不需要像题解一样令dp[0]=1,因为没有意义。

遍历顺序

遍历顺序一定是从前往后,因为本题也属于斐波那契数列题目,当前值基于他的前两个状态

打印DP数组

我们可以先推导自己认为的DP数组数值,然后打印看是否符合要求。

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第2张图片

完整版

  • 本题也是一道斐波那契数列的相关题目
class Solution {
public:
    int climbStairs(int n) {
        if(n<=2) return n;
        vector<int>dp(n+1,0);
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
};
debug测试

Line 1034: Char 34: runtime error: applying non-zero offset 4 to null pointer (stl_vector.h)

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第3张图片
这个错误信息是说试图对一个空的vector应用非零的偏移量。这个问题出在使用 dp[i] 之前没有为 dp 分配足够的空间

在C++中, std::vector 的初始大小为0,如果试图访问或修改不存在的元素(如 dp[1]dp[2]),这就会导致运行时错误

需要先调用 std::vector::resize 或者在创建 std::vector 时就指定它的大小,才能保证有足够的空间来存储元素。

修改dp初始化:vectordp(n+1,0)

空间复杂度优化写法

  • 很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化但面试中能写出版本一就够了,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化
  • 因为版本一才能体现出动规的思想精髓,递推的状态变化、
// 版本二
class Solution {
public:
    int climbStairs(int n) {
        if (n <= 1) return n;
        int dp[3];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            int sum = dp[1] + dp[2];
            dp[1] = dp[2];
            dp[2] = sum;
        }
        return dp[2];
    }
};

746.使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。

- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
  总花费为 15

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。

- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
  总花费为 6

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

思路

本题首先要明确题意。题目中没有给出楼顶的位置,但是楼顶的阶数应该是cost.size()而不是cost数组的最大下标。题意如下图所示。

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第4张图片

可以选择0或者1往上跳,每次往上跳都花费cost[i]的体力。

DP数组含义

我们要求的是到达楼顶的最小花费,dp[i]表示的就是花费。

i表示的是当前到了哪个台阶,而dp[i]的值表示的就是到i位置时候的所有花费

DP数组含义一定要搞清楚,这一点很重要,递推公式基于数组

递推公式

递推公式我们需要得到的是dp[i]。本题可以一步一个台阶或者一步两个台阶,因此,dp[i]是由dp[i-1]或者dp[i-2]跳上来的

dp[i]表示的是,跳到i位置所需要的最小花费。因为既可以从i-1跳上来,也可以从i-2,因此递推就是取这二者花费的最小值。公式推导如下图:

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第5张图片
因此公式为,dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

DP数组初始化

dp公式可以看出,最开始的dp[2]是由dp[1]和dp[0]求得。也就是说我们只需要初始化dp[1]和dp[0]

因为0和1是初始值,往上跳的时候才需要花费体力值,因此dp[0]和dp[1]的值都是0.

(DP数组的含义:dp[i]表示的是跳到i时候的花费,初始值花费就是0)

遍历顺序

本题也是爬楼梯的衍生题目,因此也是从前到后遍历。

遍历顺序补充

但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 例如:01背包,都知道两个for循环,一个for遍历物品,嵌套一个for遍历背包容量,那么,为什么不是一个for遍历背包容量,嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒序呢

这些问题都是和遍历顺序有关的,等学到了背包再进行对比。

打印DP数组

debug过程中如果出现问题,就把预期DP数组写出,再打印进行对比。

预期DP数组:

DAY42:动态规划(二)斐波那契数列+爬楼梯+最小花费爬楼梯_第6张图片

完整版

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        if(cost.size()<=1) return 0;
        int n=cost.size();
        //初始化
        vector<int>dp(n+1,0);
        //dp[0]=0;已经进行了0的初始化这两句可以不写
        //dp[1]=0;
        for(int i=2;i<=n;i++){
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

空间复杂度优化写法

// 版本二
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int dp0 = 0;
        int dp1 = 0;
        for (int i = 2; i <= cost.size(); i++) {
            int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
            dp0 = dp1; // 记录一下前两位
            dp1 = dpi;
        }
        return dp1;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 在面试中,能写出版本一就行,除非面试官额外要求 空间复杂度,那么再去思考版本二,因为版本二还是有点绕。版本一才是正常思路。

你可能感兴趣的:(刷题记录,动态规划,算法,c++,leetcode,数据结构)