DP五部曲(刚学)拿来用用、验其真假,毕竟DP这个小妖精,曾经带给我们痛苦的回忆,为了递推表达式苦苦挣扎,稍微改变题目就被按在地上狠狠摩擦,误以为自己一点灵活机变的能力都没有,对其闻风丧胆、望风而逃。如今,有了些胆识(实则是被迫营业)来直面风暴吧!少年!
本篇是基础篇,省略了斐波那契和爬楼梯这种小白题目,直接从最小费用爬楼梯下刀!
后面还会出各个问题的相应专题,今天先练个手。
注:前面标号为LC(力扣)标号,读者可对应刷题。
DP五步曲
1.明白DP数组下标含义(雨巨强调的重点)
2.推出递推表达式(自不必说)
3.确定初始值(正确的起步很重要)
4.明了遍历顺序(这个不明白,背包问题会吃大亏)
5.懂得打印DP数组验证想法(懂得自我检验而不是盲目狂抄代码)
最小费用爬楼梯
我是觉得这个题目描述很迷,不知你们有没有这种感觉哈
它那个下标有些问题,没办法用实际的例子理解透。
唯一能做的是顺应五步曲,并且充分信任你之前计算的DP数组的结果!
我这样考虑是从:你踏上第 i 级台阶时,价钱已经在之前的第(i - 1)或者第(i - 2)级台阶上付过了。
所以你在第0或者第1级上没有价格,因为根本不存在之前付钱这一说。
因为DP数组的含义是踏上第 i 级台阶支付的最小费用,因此必须从 1 ~ i 都要有数值。
最后的结果是第 s 级的最小费用,也就是最后一级台阶(那个并不存在的终点——就是要抵达所有台阶之上)。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int s = cost.size();
int dp[s+1];
dp[0] = 0, dp[1] = 0;
for(int i = 2; i <= s; i++){
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
return dp[s];
}
};
如果仍对cost的下标有问题,请参见题目原话
第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
不同路径
这题似曾相识,快速AC,老规矩,先上五步曲
class Solution {
public:
int uniquePaths(int m, int n) {
if(m == 1 & n == 1) return 1;
int dp[m+1][n+1];
dp[1][1] = 0;
for(int i = 2; i <= m; i++) dp[i][1] = 1;
for(int i = 2; i <= n; i++) dp[1][i] = 1;
for(int i = 2; i <= m; i++){
for(int j = 2; j <= n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
不同路径Ⅱ
这题思路很简单,但是一个叫做容器的东西却磨了我很久的时间。因为二维数组初始化时,列必须确定,所以这里不能用二维数组。得用容器。而且容器下标得从0开始(因为给的obstacleGrid的是从0开始的哦)
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
for(int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for(int i = 0; i < n && obstacleGrid[0][i] == 0; i++) dp[0][i] = 1;
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(obstacleGrid[i][j]) dp[i][j] = 0;
else dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
整数拆分
这一题的第一步很重要——就是如何定义DP数组?
我一开始的想法是dp[ i ]表示将 n 拆分为 i 个数字相加的最大积,但想了半个小时,发现根本没办法进行递推。
此路不通就换一条啊!傻!
对小一点的数而言,最大乘积都是拆分为两个数而来的,所以如果不加 j * (i - j)可能连前面的基础数都算不对。
因为对于 dp[i] 而言,只有两种来历,一种是 j * (i - j),一种就是 j * dp[i - j]
class Solution {
public:
int integerBreak(int n) {
int dp[n+1];
memset(dp, 0, sizeof dp);
dp[2] = 1;
for(int i = 3; i <= n; i++){
for(int j = 1; j <= i - 2; j++){
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
};
不同的二叉搜索树
没有多余的想法,就是老老实实地找规律,另外还要明白二叉搜索树的定义
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
class Solution {
public:
int numTrees(int n) {
if(n == 1) return 1;
int dp[n+1];
memset(dp, 0, sizeof dp);
dp[0] = 1, dp[1] = 1, dp[2] = 2;
for(int i = 3; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += (dp[j-1] * dp[i-j]);
}
}
return dp[n];
}
};
总结
基础篇还算比较简单的,我们DP数组的定义都是直接根据题目的问题来,它要我们求什么,我们就怎么定义,过了第一关后面的步骤就都好说了。
让我们一起期待提高篇吧!磨刀霍霍ing!
从废墟上站立起来!I totally believe in us!