动态规划总结-以Leecode股票问题(买股票的最佳时机)为例
前言
前一段时间以背包问题总结了一波动态规划的内容,最近这段时间又刷了Leecode中的股票问题,对动态规划又有了一些更深入的理解,这里再来总结一波,希望可以便人便己。
一、前面的总结
这次博客在前面进行总结,后面在进行举例分析。来个总分(总分总)类型。
“动态规划算法,本质上算是一种搜索算法,只不过其自带剪枝功能.”。(不知道是哪位大神说的了).。 深奥而精辟,本质的东西总是抽象而难于理解的。从稍微具体的角度来看,它就是重复利用以前计算过的子单元,用空间换时间的一种方式。
稍微刷过一点题的人应该都了解上面这张表,没错。这就是DP中用来存储的内存表。
当我们需要填充第(i,j)位置的时候,参考的是以前已经计算完的子单元状态。(比如说可能是上图中的被红色包围起来的蓝色部分。 )。 这里牵扯到两个问题:
(1)、当我们研究的是(i,j)状态的空间,(即准备填充(i,j)时),已经遍历的子状态是什么样的,在上图中如何分布?
答: 这个不确定。 需要根据具体的算法来,比较简单的可能是上图中的情况(如背包问题、下文中的一些股票问题)。 比较常见的还有可能是下面两种情况。
其中后向结构比较典型的有背包问题的另一种解法, 对角线结构比较经典的有最长公共子序列。
不同的算法,不同的状态定义,可能会有不同的结构。 这个要是具体的情况而定。
(2)、 如何通过已有的DP状态,算出当前的dp[i][j]?
答: 这个问题也是不确定的。 关键的问题是状态转移方程。 通过状态转移方程,就可以知道如何通过子状态计算出当前状态。
当然,状态转移方程也不是好推的,需要对题目的意思进行分析、思考。
Leecode中股票的系列问题中有一种思考方式-状态转移法(当然,这个不是适用于所有DP问题),下文中会进行说明讲解。
二、股票问题(买股票的最佳时机I-IV)
2.1 买股票的最佳时机I
2.1.1 题目描述
2.1.2 解法
class Solution {
public:
vector<int> prices;
int maxProfit(vector<int>& prices) {
int minPrice = INT_MAX;
int maxPro = 0;
for(auto tmpPrice : prices){
if(minPrice <= tmpPrice){
maxPro = max(maxPro,tmpPrice-minPrice);
}else{
minPrice = tmpPrice;
}
}
return maxPro;
}
};
代码说明:这里的代码很简单,就是维护了目前为止的最小值,然后遍历数组,依次和最小值比较,要么更新最小值,要么计算受益。 这题的解法思路有点单调栈的影子,不过并没有在栈底完全保存递增的元素,而是保存了一个最小值。 有关单调栈,刷题的时候遇到的也挺多的,以后抽时间也总结一波。
2.2 买股票的最佳时机II
2.2.1 题目描述
2.2.2 解法
这题和第一题不一样的地方在于,这里可以买多次股票(前者只能买卖一次)。 感觉上可以使用动态规划,第i天的操作,可以利用之前的结果。但是这里有个小问题,就是你也不清楚,第i天是可以买还是可以卖(不知道第i天是否持有一个股票),所以这就有点尴尬了…
其实可以稍微变通一点,前i天的买和卖的结果都记录下来,然后利用状态转换,即下图所示。
左边和右边的意思其实是一样的的,不过左边是动作转换图,右边是状态转换图。相对来说状态转换图用到的更多些,也更好理解一些。
所以,我们可以令
dp[i][0] 保存 为第i天股票数为0的最大受益;
dp[i][1] 保存为第i天股票书为1的最大受益。
所以很容易就可以得出状态转移方程:
和
最终的结果就是 dp[n][0] (最终的股票数目为0)。
基本的代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int length = prices.size();
vector<vector<int>> dp(2,vector<int>(length+1,0));
dp[0][0] = 0;
dp[1][0] = INT_MIN;
for(int i = 1;i<=length;++i){
dp[0][i] = max(dp[0][i-1],dp[1][i-1]+prices[i-1]);
dp[1][i] = max(dp[1][i-1],dp[0][i-1]-prices[i-1]);
}
return dp[0][length];
}
};
注意:代码中dp[0][i] 代表为第i天股票数为0的最大受益; 没太大影响。
2.3 买股票的最佳时机III
2.3.1 题目描述
2.3.2 解法
题目没啥大区别,只是把用户最多可以完成的交易增加到了两笔。 如果你深入理解了上个题目,这个题目应该很快就可以触类旁通出来,不就是多增加了一笔交易嘛,在增加一个状态嘛。 状态转换可以如下所示:
其中0、1代表用户持有的股票数量。 上图中不同的线代表状态之间转换的动作。 注意上面状态转换之间必须要符合一定的因果规则,即第一次卖出只能在第一次买入之后。
还有一幅从leecode上扒下来的图以供参考。
根据上图,我们可以定义以下状态:
dp[i][0][1] :代表第i天剩余0份股票的第一次卖出所带来的最大利润。
dp[i][0][2] :代表第i天剩余0份股票的第二次卖出所带来的最大利润。
dp[i][1][1] :代表第i天剩余1份股票的第一次买入所带来的最大利润。
dp[i][1][2] :代表第i天剩余1份股票的第二次买入所带来的最大利润。
然后,可以分析一波可以得到以下的动态转移方程:
然后,就是把算法化成代码实现了。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int length = prices.size();
vector<vector<vector<long long >>> dp(length+1,vector<vector<long long>>(2,vector<long long>(3,0)));
for(int i = 0;i<length+1;++i){
dp[i][0][0] = 0;
}
dp[0][0][1] = INT_MIN;
dp[0][0][2] = INT_MIN;
dp[0][1][1] = INT_MIN;
dp[0][1][2] = INT_MIN;
for(int i = 1;i<=length;++i){
dp[i][0][1] = max(dp[i-1][0][1],dp[i-1][1][1]+prices[i-1]);
dp[i][0][2] = max(dp[i-1][0][2],dp[i-1][1][2]+prices[i-1]);
dp[i][1][1] = max(dp[i-1][1][1],dp[i-1][0][0]-prices[i-1]);
dp[i][1][2] = max(dp[i-1][1][2],dp[i-1][0][1]-prices[i-1]);
}
long long maxAns = max(dp[length][0][0],dp[length][0][1]);
return max(maxAns,dp[length][0][2]);
}
};
2.4 买股票的最佳时机IV
2.4.1 题目描述
2.4.2 解法
你品,你细品,老坛酸菜,还是原来的味道。
只是,把上题目中的3变成了k, 原理还可以用原来的。
这里就不赘述具体的了。直接上代码。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int length = prices.size();
vector<vector<vector<long long >>> dp(length+1,vector<vector<long long>>(2,vector<long long>(k+1,0)));
for(int i = 0;i<length+1;++i){
dp[i][0][0] = 0;
}
for(int i = 0;i<k;++i){
dp[0][0][i+1] = INT_MIN;
dp[0][1][i+1] = INT_MIN;
}
for(int i = 1;i<=length;++i){
for(int j = 1;j<=k;++j){
dp[i][0][j] = max(dp[i-1][0][j],dp[i-1][1][j]+prices[i-1]);
dp[i][1][j] = max(dp[i-1][1][j],dp[i-1][0][j-1]-prices[i-1]);
}
}
long long maxAns = 0;
for(int i = 0;i<=k;++i){
maxAns = max(maxAns,dp[length][0][i]);
}
return maxAns;
}
};
三、 后面的概括
有始有终,最后再说点。
股票问题除了上面介绍的系列之外,leecode上还有几个,比如说带冷冻期的、要交手续费的。 最终大都可以使用动态规划解决。这里就不赘述了。
然后,
说点啥呢 …
哎,刷题,遇到动态规划,找到套路,ok,没毛病,一气呵成。 找不到方法,哎,难…
参考
【1】、买股票的最佳时机
【2】、买股票的最佳时机II
【3】、买股票的最佳时机III
【4】、买股票的最佳时机IV