1774 字
9 分钟
Day32-动态规划 part01

动态规划#

动态规划(Dynamic Programming,简称DP)是一种用于解决最优化问题的算法设计方法,其核心思想是将原问题划分为若干子问题,先解决子问题,并利用子问题的解构造原问题的解,从而避免重复计算,提高效率。

动态规划适用于满足以下两个条件的问题:

  1. 最优子结构(Optimal Substructure)
    原问题的最优解可以由子问题的最优解构成。

  2. 重叠子问题(Overlapping Subproblems)
    原问题在递归求解过程中会反复遇到相同的子问题。

动态规划五步曲:

  1. 定义状态(确定dp数组(dp table)以及下标的含义):用一个或多个变量来描述子问题的结构,一般以 dp[i]dp[i][j] 表示。
  2. 确定状态转移方程(确定递推公式):描述如何通过已知状态计算出新的状态。
  3. 确定初始条件(dp数组如何初始化):即基础状态的值,作为递推的起点。
  4. 确定计算顺序(确定遍历顺序):通常是从小到大递推。
  5. 返回最终结果(举例推导dp数组):即问题的答案对应的状态值。

斐波那契数列#

509. 斐波那契数

用递归的方式来实现斐波那契数列的实现其实很简单,直接递归调用 fib(n - 1) + fib(n - 2) 即可。 如果使用动态规划的方式来实现,我们按照动态规划五步曲来实现:

  • dp[i] 表示第 i 个斐波那契数的值。
  • 递推公式:dp[i] = dp[i - 1] + dp[i - 2]
  • 初始化dp数组:dp[0] = 0, dp[1] = 1
  • 遍历顺序:从 2n
  • 返回结果:dp[n]

因此我们动态规划的实现如下:

class Solution {
public:
    int fib(int n) {
        // 动态规划
        if (n <= 1) return n;
        // dp[i] 表示第 i 个斐波那契数的值
        // 
        vector<int> dp(n + 1, 0); // 
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
        
    }
};

为什么dp数组要初始化为 dp(n + 1, 0)#

因为我们需要计算到 n,所以我们需要一个大小为 n + 1 的数组来存储从 0n 的斐波那契数。

假设 n = 5
F(0) = 0  
F(1) = 1  
F(2) = F(1) + F(0)  
F(3) = F(2) + F(1)  
F(4) = F(3) + F(2)  
F(5) = F(4) + F(3)

从 0 到 5 一共需要 6 个数,所以 dp 数组的大小为 n + 1。
数组必须有 n + 1 个元素,才能保证 dp[0] ~ dp[n] 都能合法访问

爬楼梯#

70. 爬楼梯

我们可以把这个问题简化一下:

  • 假设我们要爬到第3层
  • 我们可以从第2层爬上来(走一步)
  • 或者从第1层爬上来(走两步)

因此我们可以推导出:

  • 因为爬到第 n 层楼梯有两种方式:
  • 从第 n - 1 层楼梯爬上来(走一步)
  • 从第 n - 2 层楼梯爬上来(走两步)
  • 从而爬到第 n 层楼梯的方式有 f(n) = f(n - 1) + f(n - 2)种方式

所以我们可以定义 dp 数组 dp[i],用来表示爬到第 i 层楼梯的方式有多少种。

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

所以这题的思路其实也就转换成了和斐波那契数列一样了。

接下来是定义初始条件:

  • dp[i] 表示第 i 层楼梯的方式
  • 因为题中描述了 n 是从 1 开始的,因此我们直接从 1 开始定义
  • dp[1] = 1,爬到第 1 层楼梯只有一种方式
  • dp[2] = 2,爬到第 2 层楼梯有两种方式:
    • 走一步到第 1 层,再走一步到第 2 层
    • 一次性走两步到第 2 层
如果一定要初始化dp[0]

由于到达第0层楼梯在题中并没有实际意义,所以我们可以根据题意,“到达第 2 层有两种方式”,反推出dp[0] = 1

遍历顺序:因为我们是一层一层往上爬的,所以很自然是从前往后遍历,再根据题意,所以我们从 3 开始遍历到 n

所以我们的整体代码如下:

class Solution {
public:
    int climbStairs(int n) {
        if (n <= 2) return n; // 如果台阶数小于等于2,直接返回n
        vector<int> dp(n + 1, 0); // dp[i]表示到达第i级台阶的方法数
        dp[1] = 1; // 到达第1级台阶的方法数为1
        dp[2] = 2; // 到达第2级台阶的方法数为2
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2]; // 到达第i级台阶的方法数为到达第i-1级台阶和第i-2级台阶的方法数之和
        }
        return dp[n]; // 返回到达第n级台阶的方法数
    }
};

使用最小花费爬楼梯#

746. 使用最小花费爬楼梯

确定转移状态方程 dp[i] 和下标 i 的含义:

  • dp[i] 表示到达第 i 级台阶的最小花费。 根据题意,我们只能选择跳 1 级或 2 级台阶,所以当我们需要跳到第 i 级台阶时:

  • 从第 i - 1 级台阶跳上来,花费 cost[i - 1] 的费用

    • dp[i] = dp[i - 1] + cost[i - 1]
  • 从第 i - 2 级台阶跳上来,花费 cost[i - 2] 的费用

    • dp[i] = dp[i - 2] + cost[i - 2]

既然我们可以从第 i - 1 级台阶跳上来,也可以从第 i - 2 级台阶跳上来,有两种选择,根据题意我们需要选择最小的费用,所以我们可以得到:

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

初始化 dp 数组:

  • 题意中:“你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是说到达第 0 个台阶是不花费的,但从第 0 个台阶 往上跳的话,需要花费 cost[0]
  • 所以我们可以定义 dp[0] = 0,表示到达第 0 个台阶的费用为 0。
  • dp[1] = 0,表示到达第 1 个台阶的费用为 0。
  • 也就是默认第一步是不花费体力的。

所以我们的整体代码如下:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size(); // 台阶数
        vector<int> dp(n + 1, 0); // dp[i]表示到达第i级台阶的最小花费
        dp[0] = 0; // 从下标为 0 或下标为 1 的台阶开始,因此支付费用为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]); // 到达第i级台阶的最小花费为到达第i-1级台阶和第i-2级台阶的最小花费之和
        }
        return dp[n]; // 返回到达第n级台阶的最小花费
    }
};
其实还是要理解题意

这里其实理解题意比较重要,跳到第 0 级台阶和第 1 级台阶是不需要花费体力的,所以我们可以直接将 dp[0]dp[1] 初始化为 0。