今天开始新的一章,动态规划,也就是DP。学习了DP基础,了解到DP相关题目可以分为4个部分,分别是:
- 定义dp数组;
- 确定状态转移方程;
- 确定dp数组初始化方式;
- 确定遍历顺序。
这4部分的顺序性很重要,后面的步骤往往依赖于前面的。
从今天的题目中学到DP的一个思想就是已知当前的位置,然后倒推当前位置的来源可能是哪些位置。
第1题(LeetCode 509. 斐波那契数)很简单,是DP最基础的题目。DP定义方面,是将dp[i]定义为第i个斐波那契数。把前两个数字0和1存放在dp数组第0位和第1位,然后从第2位开始不断计算当前位的结果,即当前位的前面两位数字的和。所以状态转移方程是dp[i] = dp[i - 1] + dp[i - 2]。直到计算到第n位,将第n位的数字作为结果返回。
class Solution {
public:
int fib(int n) {
if (n == 0) {
return 0;
}
int dp[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
dp[i] = dp[i - 2] + dp[i - 1];
}
return dp[n];
}
};
需要注意如果输入的n是0的话,就无法初始化dp数组第1位,会造成越界,所以要做特殊处理。
使用dp数组会占用过多的空间,所以也可以仅用两个int数字来保存前两位的结果。
class Solution {
public:
int fib(int n) {
if (n == 0) {
return 0;
}
int pre2 = 0, pre1 = 1;
int ans = pre1;
for (int i = 2; i <= n; ++i) {
ans = pre2 + pre1;
pre2 = pre1;
pre1 = ans;
}
return ans;
}
};
第2题(LeetCode 70. 爬楼梯)自己曾经做过,但现在又忘记了具体的DP状态转移方程。看了部分题解后AC。首先定义方面,dp[i]表示上i阶楼梯的不同方法数量。因为每次只能上1阶或2阶,所以对于i阶楼梯,它的上一步要么是第i - 1阶,要么就是第i - 2阶。从第i - 1阶走1步走到第i阶,和从第i - 2阶走2步走到第i阶这两者是不重复的,因为他们的倒数第2步之后的阶梯已经不一样了,前者在第i - 1阶,后者在第i - 2阶。所以状态转移方程就是dp[i] = dp[i - 1] + dp[i - 2]。那么将前2位初始化为1和2,之后开始按照状态转移方程向后遍历,填充数字,指导第n阶为止,将第n阶的结果作为返回值返回。
class Solution {
public:
int climbStairs(int n) {
if (n == 1) {
return 1;
}
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];
}
};
与上一题一样,也可以只用2个变量来节省空间。
class Solution {
public:
int climbStairs(int n) {
if (n == 1) {
return 1;
}
vector<int> dp(3);
dp[1] = 1;
dp[2] = 2;
int ans = dp[2];
for (int i = 3; i <= n; i++) {
ans = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = ans;
}
return ans;
}
};
这道题可以扩展一下,每次能上的台阶数不再局限于1或2,而是变为[1, m]中任意一个数。对应的解法有两种,第2种是完全背包相关解法,放在后面再讨论(day 45)。第1种解法则可以延续这道题的思路,将dp从[1, m]都初始化。对于下标x的初始化,按照这道题的思路,x的上一步可能是[0, x - 1]中的任何一步,所以将dp[0]至dp[x - 1]全部相加就得到dp[x]。而[1, m]的初始化都依赖于dp[0],要将其初始化为1。
初始化结束后,dp中[m + 1, n]的每个数y对应台阶的前一级台阶可能来自于[y - m, y - 1],所以将[y - m, y - 1]的dp值相加就得到dp[y]。
class Solution {
public:
int climbStairs(int n) {
if (n == 1) {
return 1;
}
vector<int> dp(n + 1, 0);
dp[0] = 1;
int m = 2;
int sum = dp[0];
for (int i = 1; i <= m; ++i) {
dp[i] = sum;
sum += dp[i];
}
for (int i = m + 1; i <= n; ++i) {
for (int j = i - m; j < i; j++) {
dp[i] += dp[j];
}
}
return dp[n];
}
};
第3题(LeetCode 746. 使用最小花费爬楼梯)自己AC,初看觉得很难,但也因为是DP章节所以想到用DP。定义方面,dp[i]表示到达第i阶楼梯所需要的最小花费。与上道题一样,对于第i阶楼梯,它的上一步要么是第i - 1阶楼梯,要么是第i - 2阶楼梯。所以已知了第i - 1阶和第i - 2阶楼梯的最小花费后,再令两者各自加上自己前进所需的花费,即cost[i - 1]和cost[i - 2],就得到了2种路线和对应的花费。从两者中取较小的一个当做第i阶的花费即可,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]。dp前2个数字初始化为0,然后从第3个(下标2)开始遍历到“楼梯数组长度 + 1”个(下标楼梯数组长度),将其作为结果返回。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size() + 1, 0);
for (int i = 2; i <= cost.size(); ++i) {
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp.back();
}
};
题解则是将dp[i]定义为到达第i阶楼梯后,再从第i阶楼梯出发所需要的最少花费。所以状态转移方程变为了dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]。在这种定义下,dp数组的前两个数字就需要填充为cost[0]和cost[1],然后从下标2一直遍历到下标楼梯数组长度为止。最后从dp数组的末尾两个数字中,取较小的一个当做答案,因为从这两个点出发都可以到达目标位置。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size() + 1);
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];
}
return min(dp[cost.size() - 1], dp[cost.size() - 2]);
}
};
由于这道题也只需要保存当前位置的前两个结果,所以也可以像前面两个题一样,用两个数字来代替dp数组,这里就不再写了。
二刷:用了两行数组来分别表示到达当前台阶的最后一步是一级还是两级,但没必要,用一行数组表示两者的最小值就可以了。