股票利益最大化问题:
给定股票价格序列,规定(买入、卖出)的最多次数K,求最大的总利润。(一次买入和卖出为完整交易)
1.K=1 Leetcode121
记录遍历到第i天的时候之前的股票价格最小值,那么如果当天卖出,能取得最大利润肯定是当前股票价格减去之前最低的股票价格
最后取遍历的最大值。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int max_profit = 0;
int pre_min = INT_MAX;
for(int i=0;i<prices.size();i++)
{
max_profit=max(max_profit,prices[i]-pre_min);
pre_min = min(pre_min,prices[i]);
}
return max_profit;
}
};
2.K=2,最多(买入、买去)两次LeetCode123
相当于找两个利润最大的价格区间,并且这两个区间是不重叠的,也就是说这两个区间左右一个,那么如果在第i天把区间分成两部分,所以借用k=1的思路,每部分分别求各自的最大利益,然后遍历每天,求左右两部分最大的两个利益,最后去最大值即可。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=0)
return 0;
int len=prices.size();
int *dp_left=new int[len];
int *dp_right=new int[len];
dp_left[0]=0;
int min_val=INT_MAX,max_profit=0;
for(int i=0;i<len;i++)
{
max_profit=dp_left[i]=max(max_profit,prices[i]-min_val);
min_val=min(min_val,prices[i]);
}
max_profit=0;
int high_val=-1;
for(int i=len-1;i>=0;i--)
{
max_profit=dp_right[i]=max(max_profit,high_val-prices[i]);
high_val=max(high_val,prices[i]);
}
max_profit=0;
for(int i=0;i<len;i++)
{
max_profit=max(max_profit,dp_left[i]+dp_right[i]);
}
return max_profit;
}
};
3.不限定买卖次数,即K=
+
∞
+\infty
+∞ Leetcode122:
因为不限制买卖次数,则此时每一个上升区间的两端,都分别可以进行买入和卖出,只需要统计每个上升区间的落差就行。
class Solution {
public:
int maxProfit(vector<int>& prices) {
//寻找连续的波峰和波谷即可,不跨越任何一个波峰
if(prices.size()<=0)
return 0;
int maxProfit=0;
int valley=prices[0];
int peak=prices[0];
int i=0,len=prices.size()-1;
while(i<len)
{
//下坡
while(i<len && prices[i+1]<=prices[i])
i++;
valley = prices[i];
//上破
while(i<len && prices[i+1]>=prices[i])
i++;
peak = prices[i];
maxProfit += peak-valley;
}
return maxProfit;
}
};
简单形式:不需要显示的求出每个上升区间的落差,只要是上升的,那么在该区间内用后一天价格减去当天价格,对所有差求和就是区间的落差。
class Solution {
public:
int maxProfit(vector<int>& prices) {
//和寻找连续的波峰波谷是同样的思路,但是不用具体找到确切的波峰和波谷的位置
if(prices.size()<=0)
return 0;
int maxProfit=0;
int i=0,len=prices.size()-1;
while(i<len)
{
//只要找到上升的路径,就不断累加
if(prices[i+1]>=prices[i])
maxProfit += prices[i+1]-prices[i];
i++;
}
return maxProfit;
}
};
大杀器-通用解法
使用动态规划的思想:
dp [n][k][flag]:表示在第n天时,最多买入k次股票,且目前手里持有股票的状态是flag。其中天数从0开始, flag=0表示目前手里没有股票,flag=1表示手里还有股票,其中在股票买入的时候,对k进行更新。
状态转移方程:
dp [n][k][0]=max(dp [n-1] [k] [0],dp [n-1] [k] [1]+prices[n])
解释:今天(第n天)手里没有股票,有两种可能:
解释:今天(n天)手里没有股票,且最多买入了k次股票,有两种可能:
1.昨天手里就没有股票,也今天没有进行买卖操作(k不会变),所以今天手里还是没有股票,收益不变,仍为dp[n-1][k][0]
2.昨天手里有股票,今天把手里价值为price[n]的股票卖了(k也不会变),收益变成dp[n-1][k][1]+price[n] (买入股票时才修改k)
dp [n] [k] [1]=max(dp [n-1] [k] [1],dp [n-1] [k-1] [0]-prices[n])
解释:今天(n天)手里有股票,有两种可能:
1.昨天手里就有股票,今天没有进行买入操作(k不会变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
2.昨天手里没有股票,今天买入价值为price[n]的股票(k发生改变),收益变成dp[n-1][k-1][0]-price[n]。(以买入股票作为一个交易结束,所以k-1)
初始化:
dp[i][0][0]=0表示到第i天,最多使用了0次交易(就是说并未买入任何股票),那肯定就是0
dp[i][0][1]=
−
∞
-\infty
−∞表示到第i天,并未买入任何股票的情况下,手里持有股票,这是肯定不正确的,置为负无穷
dp[0][k][0]=0表示第0天时,最多使用了k次交易(即买入了k次股票,其实最多也就买入了一次)且手里没有股票,肯定是0
dp[0][k][1]=-prices[0] 表示第0天时,最多使用了k次交易(即买入了k次股票,其实最多也就买入了一次)且手里已经有股票了,表明在在第一天已经买入股票了。(此时k必须大于1)
PS:对于以卖出时候更新k的解法,读者可自行分析,参考件本文最下方;
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=0)
return 0;
int len=prices.size();
int K_max=2;
vector<vector<vector<int>>> dp(len,vector<vector<int>>(K_max+1,vector<int>(2)));
//K为任意正整数的通用解法-状态机+三维DP(买入股票时,k发生改变)
for(int i=0;i<len;i++)
{
dp[i][0][0]=0;
dp[i][0][1]=-INT_MAX;
}
for(int k=K_max;k>=1;k--)
{
dp[0][k][0]=0;
dp[0][k][1]=-prices[0];
}
//接下来对第0天之后的情况使用状态转移方程计算即可
for(int n=1;n<len;n++)
{
for(int k=K_max;k>=1;k--)
{
dp[n][k][0]=max(dp[n-1][k][0],dp[n-1][k][1]+prices[n]);
dp[n][k][1]=max(dp[n-1][k][1],dp[n-1][k-1][0]-prices[n]);
}
}
//在最后一天(len-1)最多使用K_max次交易,肯定是把手里股票卖光收益更高(不然留着手里发霉啊?)
//所以是dp[len-1][K_max][0],而非dp[len-1][K_max][1]
//其次,最多使用K_max次交易交易肯定是包括最多使用K_max-1次,K_max-2次交易的情况。
return dp[len-1][K_max][0];
}
};
ps:对于 K=+ ∞ \infty ∞的情况,此时K已经毫无意义,可以直删除该维度,代码精简如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=0)
return 0;
int len=prices.size();
vector<vector<int>> dp(len,vector<int>(2));
for(int i=0;i<len;i++)
{
dp[i][0]=0;
dp[i][1]=-INT_MAX;
}
dp[0][1]=-prices[0];
for(int n=1;n<len;n++)
{
dp[n][0]=max(dp[n-1][0],dp[n-1][1]+prices[n]);
dp[n][1]=max(dp[n-1][1],dp[n-1][0]-prices[n]);
}
return dp[len-1][0];
}
};
此外,对于通用的形式,可以看见循环中,第一维度的变化是步长为1递减的,所以可以进一步对通用形式进行空间优化。不然在总天数和k很大的时候无法通过LeetCode188:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int N=prices.size();
if(N<=1) return 0;
if(k>N/2)
k=N/2+1;
//套用通用解法模板(买入时更新k)
vector<vector<int>> dp(k+1,vector<int>(2));
for(int m=0;m<=k;m++)
{
dp[m][0]=0;
dp[m][1]=-prices[0];
}
dp[0][1]=-INT_MAX;
//因为n都是每次步长为1递减的,所以为了优化内存,可以把三维dp变为二维dp
for(int n=1;n<N;n++)
{
for(int m=k;m>=1;m--)
{
dp[m][0]=max(dp[m][0],dp[m][1]+prices[n]);
dp[m][1]=max(dp[m][1],dp[m-1][0]-prices[n]);
}
}
return dp[k][0];
}
};
延伸1.如每次完整的交易需要交手续费(Leetcode714),同样可以使用通用解法:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len=prices.size();
if(len<=1) return 0;
vector<vector<int>> dp(len,vector<int>(2));
for(int i=0;i<len;i++)
{
dp[i][0]=0;
dp[i][1]=-INT_MAX;
}
//规定在卖出股票的时候扣手续费,所以第一天手里如果有股票,不需要扣手续费
//ps:也可以规定在买入股票的时候扣手续费,所以第一天手里如果有股票,需要扣手续费:
//即变成dp[0][1]=-prices[0];
dp[0][1]=-prices[0];
for(int i=1;i<len;i++)
{
//每次完整的交易才需要交一个手续费,所以是在卖出股票的时候交手续费
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]); //如果规定买入时扣手续费,那么在这里减去fee
}
return dp[len-1][0];
}
};
延伸2.每次卖出股票后,后一天不允许买入股票LeetCode309:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=1)
return 0;
int len=prices.size();
vector<vector<int>> dp(len,vector<int>(2));
//第0天手里没有股票和有股票,收益分别是0和-prices[0]
dp[0][0] = 0;
dp[0][1] = -prices[0];
//第1天手里没有股票的两种情况:1.根本没买过 2.在第0天买了,第二天卖了
//第1天手里有股票的两种情况:1.第0天买的 2.在第1天买的
dp[1][0] = max(0,prices[1]-prices[0]);
dp[1][1] = max(-prices[0],-prices[1]);
for(int n=2;n<len;n++)
{
dp[n][0]=max(dp[n-1][0],dp[n-1][1]+prices[n]);
//当天手里有股票,可能昨天就有但是没进行任何操作,或者前天卖掉股票后手里没股票,然后昨天被冻结了
dp[n][1]=max(dp[n-1][1],dp[n-2][0]-prices[n]);
}
return dp[len-1][0];
}
};
拓展:以卖出时候更新k值:
dp [n][k][flag]:表示在第n天时,最多卖出了k次股票,目前手里持有股票的状态是flag,
flag=0表示目前手里没有股票,flag=1表示手里还有股票
dp [n][k][0]=max(dp[n-1][k][0],dp[n-1][k-1][1]+price[n])
解释:今天(n天)手里没有股票,且最多卖出了k次股票,有两种可能:
1.昨天手里就没有股票,且今天没有进行买卖操作(k不会变),所以今天手里还是没有股票,收益不变,仍为dp[n-1][k][0]
2.昨天手里有股票,今天把手里价值为price[n]的股票卖了(k会发生变化),收益变成dp[n-1][k][1]+price[n]
dp [n][k][1]=max(dp[n-1][k][1],dp[n-1][k][0]-price[n])
解释:今天(n天)手里有股票,有两种可能:
1.昨天手里就有股票,今天没有进行买入操作(k不会变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
2.昨天手里没有股票,今天买入价值为price[n]的股票(k不会变),收益变成dp[n-1][k-1][0]-price[n]。
初始状态:
dp[i][0][0]=0表示到第i天,最多使用了0次交易(就是说并未卖出任何股票),那肯定就是0
dp[i] [0] [1]=max{-price[z]} (其中z=0,1,2…i) 表示到第i天,并未卖出任何股票的情况下,手里持有股票,这说明买入了一次股票,那么有可能其实在第0天第i天之间任何一天买入的股票,为了保证dp[i][0][1]尽量最大(因为本身问题就是一个最大化的问题,所以令其该天及之前之前最小价格的负数)。
dp[0][k][0]=0表示第0天时,最多使用了k次交易(即卖出了k次股票,其实最多就买入一次)且手里没有股票,肯定是0(有可能第一天根本就没买,也有可能当天买当天卖,反正都是0)
dp[0][k][1]=-prices[0] 表示第0天时,最多使用了k次交易(即卖出了k次股票,其实最多也就卖出了一次)且手里已经有股票了,说明在卖出股票后又买了一次股票,收益肯定就是-prices[0] (由上面的分析可知,这里k等于0也是一样的)
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=0)
return 0;
int len=prices.size();
int K_max=2;
vector<vector<vector<int>>> dp(len,vector<vector<int>>(K_max+1,vector<int>(2)));
int min_val=INT_MAX;
for(int i=0;i<len;i++)
{
min_val=min(min_val,prices[i]);
dp[i][0][0]=0;
dp[i][0][1]=-min_val;
}
for(int k=K_max;k>=1;k--)
{
dp[0][k][0]=0;
dp[0][k][1]=-prices[0];
}
//接下来对第0天之后的情况使用状态转移方程计算即可
for(int n=1;n<len;n++)
{
for(int k=K_max;k>=1;k--)
{
dp[n][k][0]=max(dp[n-1][k][0],dp[n-1][k-1][1]+prices[n]);
dp[n][k][1]=max(dp[n-1][k][1],dp[n-1][k][0]-prices[n]);
}
}
return dp[len-1][K_max][0];
}
};