动态规划
动态规划(Dynamic Programming,简称DP)是一种用于解决最优化问题的算法设计方法,其核心思想是将原问题划分为若干子问题,先解决子问题,并利用子问题的解构造原问题的解,从而避免重复计算,提高效率。
动态规划适用于满足以下两个条件的问题:
最优子结构(Optimal Substructure)
原问题的最优解可以由子问题的最优解构成。重叠子问题(Overlapping Subproblems)
原问题在递归求解过程中会反复遇到相同的子问题。
动态规划五步曲:
- 定义状态(确定dp数组(dp table)以及下标的含义):用一个或多个变量来描述子问题的结构,一般以
dp[i]或dp[i][j]表示。 - 确定状态转移方程(确定递推公式):描述如何通过已知状态计算出新的状态。
- 确定初始条件(dp数组如何初始化):即基础状态的值,作为递推的起点。
- 确定计算顺序(确定遍历顺序):通常是从小到大递推。
- 返回最终结果(举例推导dp数组):即问题的答案对应的状态值。
斐波那契数列
用递归的方式来实现斐波那契数列的实现其实很简单,直接递归调用 fib(n - 1) + fib(n - 2) 即可。 如果使用动态规划的方式来实现,我们按照动态规划五步曲来实现:
dp[i]表示第i个斐波那契数的值。- 递推公式:
dp[i] = dp[i - 1] + dp[i - 2]。 - 初始化dp数组:
dp[0] = 0, dp[1] = 1。 - 遍历顺序:从
2到n。 - 返回结果:
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 的数组来存储从 0 到 n 的斐波那契数。
假设 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] 都能合法访问爬楼梯
我们可以把这个问题简化一下:
- 假设我们要爬到第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级台阶的方法数
}
};使用最小花费爬楼梯
确定转移状态方程 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。

