Day33 动态规划:理论基础 509.斐波那契数 70.爬楼梯 746.使用最小花费爬楼梯

122 阅读5分钟

理论基础

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

动规五部曲:

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

一般人都是凭感觉做题。

509.斐波那契数

题目链接:509.斐波那契数

难度指数:😀😐

很简单的动规入门题,但简单题是用来掌握方法论的,还是要用动规五部曲来分析。

一般来说,斐波那契数用递归来写 (代码比较简单)

不过本题,我们要使用**动态规划(DP)**的思想来求斐波那契数列。

1️⃣ 确定dp数组(dp table)以及下标的含义

在做动态规划题目时,都要定义一个一维二维数组 dp[ ](用来做状态转移)

dp[i]:第i个斐波那契数值为 dp[i]

2️⃣ 确定递推公式 (核心

本题很简单,因为**题目已经把递推公式直接给我们了:

状态转移方程(递推公式) 为 dp[i] = dp[i - 1] + dp[i - 2];

3️⃣ dp数组如何初始化

4️⃣ 确定遍历顺序

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

(很多人做动态规划的题目,会很自然地认为一定是从前往后遍历,其实有些题目是从后向前遍历等,具体情况具体分析)

5️⃣ 打印dp数组

这一步主要是用来deBug的,

在力扣上写代码如果通过不了,把dp数组打印出来,看看和我们想象中的dp数组的样子是否一样。

AC代码: (核心代码模式)

 class Solution {
 public:
     int fib(int N) {
         if (N <= 1) return N;
         //确定DP数组及下标的含义
         vector<int> dp(N + 1);  //为啥是N + 1?    答:N + 1是数组长度,下标是0 ~ N
         //DP数组如何初始化
         dp[0] = 0;
         dp[1] = 1;
         //遍历顺序:从前到后
         for (int i = 2; i <= N; i++) {
             //
             dp[i] = dp[i - 1] + dp[i - 2];
         }
         return dp[N];
     }
 };

AC代码: (核心代码模式)

 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];
     }
 };

很多动态规划的题目都可以做状态压缩,因为当前状态只依赖于前面2个状态;或者说当前层只依赖于上一层,那我们就维护2层就可以了。

70.爬楼梯

题目链接:70.爬楼梯

难度指数:😀😕

比上一题难一点 ,不过依旧是 入门题

本题大家先自己想一想,之后会发现,和 斐波那契数 有点关系。

33.01.png

题目思路:(动规五部曲)

1️⃣ 确定dp数组(dp table)以及下标的含义

dp[i]:到达 i 级台阶有 dp[i] 种方法。

2️⃣ 确定递推公式

这道题相比斐波那契数那道题,难在递推公式需要自己推出来(抽象问题的能力)

dp[i] = dp[i - 2] + dp[i - 1];

3️⃣ dp数组如何初始化

dp[1] = 1; dp[2] = 2; (本来想dp[0] = 0,有些题解还dp[0] = 1呢❌,但题目要求n > 0)

4️⃣ 确定遍历顺序

从前往后遍历,因为 dp[i] 依赖前面2个状态。

5️⃣ 打印dp数组

思考:debug的时候,dp数组如何打印?

AC代码: (核心代码模式)

 class Solution {
 public:
     int climbStairs(int n) {
         if (n <= 1) {
             return n;
         }
         vector<int> dp(n + 1);
         dp[1] = 1;
         dp[2] = 2;
         for (int i = 3; i <= n; i++) {
             dp[i] = dp[i - 1] + dp[i - 2];
         }
         return dp[n];
     }
 };

746.使用最小花费爬楼梯

题目链接:746.使用最小花费爬楼梯

难度指数:😐☹️😟

比上一题难 !

这道题目力扣改了题目描述了,现在的题目描述清晰很多,相当于明确说:第一步是不用花费的

eg:

输入:cost = [10,15,20] 输出:15

解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

33.03.png

一开始站在 0号 台阶 or 一开始站在 1号 台阶,不花费体力值

🦄只有往上跳,才需要花体力。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,开始往上跳才要花费体力值。

题目思路:(动规五部曲)

1️⃣ 🦄确定 dp数组 以及 下标含义

dp[i]:到达第 i 个台阶所花费的最少体力为 dp[i] 。 (注意这里认为是第一步一定是要花费,现在题目描述清晰了

dp数组的含义很重要,后面哪一步卡壳了,都要先明确dp数组的含义是什么

2️⃣ 确定递推公式

可以有两个途径得到 dp[i] ,一个是 dp[i-1] ;一个是 dp[i-2]

说人话:dp[i] 是 由 dp[i - 1] 跳一步得到 dp[i];或者由 dp[i - 2] 跳两步得到 dp[i]

dp[i - 1] 往上跳的花费是 cost[i - 1]dp[i - 1] + cost[i - 1]

dp[i - 2] 往上跳的花费是 cost[i - 2]dp[i - 2] + cost[i - 2]

一定是选最小的,所以 dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];

注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值)

3️⃣ dp数组如何初始化

很多人以为动态规划只需要想清楚递推公式,其他的就简单。

其实不然,dp数组如何初始化,遍历顺序也很重要。

(如果只认为动态规划题目里面,递推公式最重要,那说明题目做得不够多。)

dp[3]由前两个即(dp[1]和dp[2])推出来;同理dp[4],……

说明整个递推公式的基础在 dp[0] 和 dp[1]

只需要初始化dp[0]和dp[1]:

一开始选择站在0号台阶,所需要的花费是0;一开始站在1号台阶,所需要的花费也是0。

dp[0] = 0; dp[1] = 0; 一开始就到达0 和 1都是不需要花费的

4️⃣ 确定遍历顺序

从前向后遍历

(不是所有题目都是从前向后;有的从后向前;有的从下往上)

5️⃣ 打印dp数组

[1,100,1,1,1,100,1,1,100,1]

33.02.png

题目描述修改前:

AC代码: (核心代码模式)

 class Solution {
 public:
     int minCostClimbingStairs(vector<int>& cost) {
         vector<int> dp(cost.size());
         dp[0] = cost[0];
         dp[1] = cost[1];
         for (int i = 2; i < cost.size(); i++) {
             dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
             cout << dp[i] << endl;
         }
         // 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
         return min(dp[cost.size() - 1], dp[cost.size() - 2]);
     }
 };