DP简介
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
写动规题目,代码出问题很正常!
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
发出这样的问题之前,其实可以自己先思考这三个问题:
这道题目我举例推导状态转移公式了么?
我打印dp数组的日志了么?
打印出来了dp数组和我想的一样么?
509.斐波那契数列
用一个一维dp数组来保存递归的结果
确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
确定递推公式
为什么这是一道非常简单的入门题目呢?
因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
int dp[2];
dp[0] = 0;
dp[1] = 1;
int sum =0;
for (int i = 2; i <= N; i++) {
sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return sum;// dp[1];
}
};
746.使用最小花费爬楼梯
/*
1.确定dp数组以及下标的含义
使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。
dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。(注意这里认为是第一步一定是要花费)
2.确定递推公式
3.dp数组如何初始化
4.确定遍历顺序
5.举例推导dp数组
*/
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];
}
// 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
return min(dp[cost.size() - 1], dp[cost.size() - 2]);
}
};
42. 接雨水【困难】【单调栈章节用动规解决】
//这个图就是大厂面试经典题目,接雨水! 最常青藤的一道题,面试官百出不厌!
//每次遍历列的时候,还要向两边寻找最高的列
/*
动态规划解法
在双指针解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算。
当前列の雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。
为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。
but if 我们把每一个位置的左边最高高度记录在一个数组上( maxLeft ),右边最高高度记录在一个数组上( maxRight ),这样就避免了重复计算,这就用到了动态规划。
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
这样就找到递推公式。
*/
class Solution {
public:
int trap(vector<int>& height) {
if (height.size() <= 2) return 0;//!!!!!!
vector<int> maxLeft(height.size(), 0);!!!!!!注意这里是指定 vector 大小和值的方法,注意此处对每个值都赋值了(虽然后面又对重点位置单独赋值了),不同于84题!
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
// 记录每个柱子左边柱子最大高度
maxLeft[0] = height[0];
for (int i = 1; i < size; i++) {
maxLeft[i] = max(height[i], maxLeft[i - 1]);
}
// 记录每个柱子右边柱子最大高度
maxRight[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
maxRight[i] = max(height[i], maxRight[i + 1]);
}
// 求和
int sum = 0;
for (int i = 0; i < size; i++) {
int count = min(maxLeft[i], maxRight[i]) - height[i];
if (count > 0) sum += count;//!!!!!!
}
return sum;
}
};
84. 柱状图中最大的矩形【困难】【单调栈章节用动规解决】
// // 本题**动态规划**的写法整体思路和42. 接雨水是一致的,但要比42难一些。
// // 难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。
// // 所以需要循环查找,也就是下面在寻找的过程中使用了 while ,详细请看下面注释,整理思路在题解:42. 接雨水中已经介绍了。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeftIndex(heights.size());//是记录下标的数组
vector<int> minRightIndex(heights.size());
int size = heights.size();
// 记录每个柱子 左边第一个小于该柱子的下标
minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
for (int i = 1; i < size; i++) {// i 范围:[ 第二个,最后一个]
int t = i - 1;
// 这里不是用if,而是不断向左寻找的过程
while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
minLeftIndex[i] = t;//t 退出循环之时,就是符合条件的下标值
}
// 记录每个柱子 右边第一个小于该柱子的下标
minRightIndex[size - 1] = size; //!!!! !!!注意这里初始化,防止下面while死循环
for (int i = size - 2; i >= 0; i--) {// i 范围:[ 倒数第二个~--~第一个]
int t = i + 1;
// 这里不是用if,而是不断向右寻找的过程
while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
minRightIndex[i] = t;
}
// 求和
int result = 0;
for (int i = 0; i < size; i++) {
int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);//末项减首项除以公差加一,再减二
result = max(sum, result);
}
return result;
}
};