股票买卖系列问题
说了这么多年的“状态转移方程”这个词,今天似乎有了新的理解。。。动态规划问题可以有等价的状态机。
以前更多的情况是在一个数组中根据相应的转移条件计算当前的值,这一系列问题,需要维护多个数组,正好可以很方便的画出状态机的图。
从易到难记录一下leetcode中这个系列的题。
121 Best Time to Buy and Sell Stock
只能进行至多一次买卖,问最多利润。
思路:一个min变量保存当前最小的价格,一个max变量保存当前最大的利润,遍历一遍,更新max和min得到最终结果。
int maxProfit(int* prices, int pricesSize) {
int max = 0;
int min = prices[0];
for(int i = 1; i < pricesSize; i++)
{
if (prices[i] - min > max) max = prices[i] - min;
if (prices[i] < min) min = prices[i];
}
return max;
}
122. Best Time to Buy and Sell Stock II
可以进行无数次买卖
思路:将所有后一个减前一个的差为正的都加起来。
int maxProfit(int* prices, int pricesSize) {
int sum = 0,t;
for (int i = 1; i < pricesSize; i++)
{
t = prices[i] - prices[i-1] ;
if (t > 0)
sum += t;
}
return sum;
}
309. Best Time to Buy and Sell Stock with Cooldown
可以进行无数次买卖,但是每次卖出后需要休息一天才可以再次买入。
思路:
可以按照题意画出状态转移图
分别使用三个数组表示这三个节点的状态,则
hold[i] = max(wait2[i-1] – prices[i], hold[i-1])
wait1[i] = hold[i-1] + prices[i]
wait2[i] = max(wait1[i-1], wait2[i-1])
每次计算出的都是当前天,如果股票交易处于这几个节点时,可以取到的最大值。
每一个节点的值仅与上一次这些节点的值有关,所以可以简化空间复杂度,仅保留上一次的节点值即可。
初始化:初始化时,弄错了很多次,i(也就是天的计数)应该从1开始,而不是0,。hold_old初始化应该为-prices[0],因为第一次出现hold状态是一定是前一次购买,不会是从hold状态到达的hold状态。wait1_old应初始化为负无穷,因为一开始不会出现这个状态,再次更新时一定是要更新的,无论正负都是要得到这个值,开始我初始化为0,出现了负数无法更新的状况。
int maxProfit(int* prices, int pricesSize) {
int hold = 0, hold_old = -prices[0], wait1 = 0, wait1_old = -1000000,wait2 = 0, wait2_old = 0;
for (int i = 1; i < pricesSize; i++)
{
hold = wait2_old - prices[i] > hold_old ? wait2_old - prices[i] : hold_old;
wait1 = hold_old + prices[i];
wait2 = wait1_old > wait2_old ? wait1_old : wait2_old;
hold_old = hold;
wait1_old = wait1;
wait2_old = wait2;
}
return wait1 > wait2 ? wait1 : wait2;
}
714. Best Time to Buy and Sell Stock with Transaction Fee
可以无数次买卖,但是每次卖出都需要缴纳手续费(一定为正)。
思路:
参考上一道题,去掉休息一天的条件,修改状态转移图为两个节点,由hold到wait时的利润减少fee。
int maxProfit(int* prices, int pricesSize, int fee) {
int hold = 0, hold_old = -prices[0], wait = 0, wait_old = 0;
for (int i = 1; i < pricesSize; i++)
{
hold = wait_old - prices[i] > hold_old ? wait_old - prices[i] : hold_old;
wait = hold_old + prices[i] - fee > wait_old ? hold_old + prices[i] - fee : wait_old;
hold_old = hold;
wait_old = wait;
}
return wait;
}
123. Best Time to Buy and Sell Stock III
最多可以买卖2次,求最大利润。
思路:
依旧画出动态转移图
有了之前的方法状态转移方程就很好写出来了
s1 = max (s1,-prices[i])
s2 = max (s2, s1+prices[i])
s3 = max (s3, s2-prices[i])
s4 = max (s4,s3+prices[i]);
由于这个状态只与前一个节点有关,更新时只需要从后往前更新便可以不用像前两道题一样保存旧值。
初始化要注意,将s2和s4保存成0,否则只买卖一次会出错。
int maxProfit(int* prices, int pricesSize) {
int s1=INT_MIN,s2=0,s3=INT_MIN,s4=0;
for(int i=0;i<pricesSize;++i)
{
s4 = s4>s3+prices[i]?s4:s3+prices[i];
s3 = s3>s2-prices[i]?s3:s2-prices[i];
s2 = s2>s1+prices[i]?s2:s1+prices[i];
s1 = s1>-prices[i]?s1:-prices[i];
}
return s4;
}
188. Best Time to Buy and Sell Stock IV
限制最多买卖k次,求最大的利润。
思路:
首先,当k大于天数的一半的时候,我们可以将它等同于无数次买卖求最大利润的情况。
然后,当k小于天数一半的时候,我们继续沿用上一道题的思路,构造一个2k个节点单向的状态机。可以看出,状态是重复的买和卖,也就是s3,s4和s1,s2状态转移方式相同,可以使用for循环进行更新。注意:逆序。
int maxProfit(int k, int* prices, int pricesSize) {
if (k == 0) return 0;
int s1[10000], s2[10000];
if (k > pricesSize / 2)
{
int sum = 0;
for (int i = 1; i < pricesSize; i++)
{
if (prices[i] > prices[i - 1]) sum += prices[i] - prices[i - 1];
}
return sum;
}
for (int i = 0; i <= k; i++)
{
s1[i] = INT_MIN;
s2[i] = 0;
}
for (int i = 0; i < pricesSize; i++)
{
for (int j = k; j > 0; j--)
{
s2[j] = s2[j] > s1[j] + prices[i] ? s2[j] : s1[j] + prices[i];
s1[j] = s1[j] > s2[j-1] - prices[i] ? s1[j] : s2[j - 1] - prices[i];
}
}
return s2[k];
}