【剑指offer】面试题63 六种买卖股票问题

本文结合剑指offer的63题与leecode上的六种买卖股票问题进行了总结,综合来说,就是需要不断地考虑在不同的条件下,dp[i][k][j]的产生条件,往往为前一位与前一位买卖的相关操作的最大值。

1.第一种股票问题(单次买卖)

  • 动态规划实现第一类股票问题,单次买入卖出如果将最低收益设为0,那么初始值为0;如果考虑负收益 ,则初始值为INT_MIN

  • 思路1:动态规划,计算每个位置的卖出价最大值(如果将0作为最小值而不考虑负值时)

  • dp[i][0]代表第i + 1天没有持股票,dp[i][1]代表第i + 1天持有股票
  • 初始化:
    dp[0][0] = 0;       第一天没有持股,这时候相当于没有买入,故为0
    dp[0][1] = -prices[0];  第一天持股,相当于买入,这时候为 - pricrs[0]
  • 状态转移方程:
    未持股 = max(昨天没有持股今天维持现状, (昨天持股,今天卖出))
    dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
    持股 = max(昨天持股今天维持现状,(之前没有交易,今天买入))【因为只能完成一笔交易,故今天买入,则前面就相当于一直没有进行利润为0】
    dp[i][1] = max(dp[i - 1][1], -1 * prices[i])(也相当于一直保持所有的dp[i][1]为买入价的最小值)
int MaxDiff1_1(const int* numbers, unsigned length)
{
	if (numbers == nullptr && length < 2)
		return 0;

	vector<vector<int>> dp(length, vector<int>(2));
	dp[0][0] = INT_MIN;
	dp[0][1] = -1 * numbers[0];
	for (int i = 1; i < length; i++)
	{
		dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + numbers[i]);
		dp[i][1] = max(dp[i - 1][1], 0 - numbers[i]);
	}

	return dp[length - 1][0];
}
  • 思路2:记录前面的最小值,不断地计算最大效益(注意不要只考虑正收益的最大值,还要包括负收益的)
int MaxDiff1_2(const int* numbers, unsigned length)
{
	if (numbers == nullptr && length < 2)
		return 0;

	int minValue = INT_MAX;
	int maxDiff = INT_MIN;

	for (int i = 0; i < length; i++)
	{
		if (numbers[i] < minValue)
		{
			maxDiff = max(maxDiff, numbers[i] - minValue);
			minValue = numbers[i];
		}
		else
			maxDiff = max(maxDiff, numbers[i] - minValue);
	}
	
	return maxDiff;
}

2.第二种股票问题(多次买卖)

  • dp[i][0]代表第i+1天没有持股票,dp[i][1]代表第i+1天持有股票
  • 初始化:
    dp[0][0] = 0;        第一天没有持股,这时候相当于没有买入,故为0
    dp[0][1] = -prices[0];   第一天持股,相当于买入,这时候为-prices[0]
  • 状态转移方程:
    没有持股 =max(昨天没有持股今天维持现状 , 昨天持股,今天卖出)
    dp[i][0] = max(dp[i-1][0],dp[i-1][1] + prices[i]);
    持股 = max(昨天持股今天维持现状,(昨天没有持股,今天买入))【相对于之前,这里改变了】(不需要存最小值了)
    dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
int MaxDiff2_1(const int* numbers, unsigned length)
{
	if (numbers == nullptr && length < 2)
		return 0;

	vector<vector<int>> dp(length, vector<int>(2));
	dp[0][0] = INT_MIN;
	dp[0][1] = -1 * numbers[0];
	for (int i = 1; i < length; i++)
	{
		dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + numbers[i]);
		dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - numbers[i]);
	}

	return dp[length - 1][0];
}

3.第三种股票问题(多次买卖,含冷冻期)

  • dp[i][0]代表第i+1天没有持股票,dp[i][1]代表第i+1天持有股票
  • 初始化:(由于有了买入冷却期,所以在冷却期k前的所有时间dp[i][1]都应该为-price[i],下面以k=1为例子)
    dp[0][0] = 0;                     //第1天没有持股,这时候相当于没有买入,故为0
    dp[1][0] = max(0,prices[1]-prices[0]); //第2天没有持股,这时候max(第一天没持股,第一天持股第二天卖出)
    dp[0][1] = -prices[0];               //第1天持股维持,相当于买入,这时候为-prices[0]
    dp[1][1] = max(-prices[0],-prices[1]); //第2天持股,这时候max(第一天持股维持,第一天没持股第二天买入)
  • 状态转移方程:
    没有持股 =max(昨天没有持股今天维持现状 , (昨天持股,今天卖出))
    dp[i][0] = max(dp[i-1][0],dp[i-1][1] + prices[i]);
    持股 = max(昨天持股今天维持现状,(前天没有持股,今天买入))即:第i天要买的时候,要从前天的状态进行判断
    dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]);
class Solution 
{
public:
	int maxProfit3_1(vector<int>& prices) 
	{
		//1. k充当冷却期间
		int k = 1;
		if (prices.size() < 2)
			return 0;
		vector<vector<int>> dp(prices.size(), vector<int>(2));
		//2. 0位置的初始化
		dp[0][0] = 0;
		dp[0][1] = 0 - prices[0];
		//3. 状态转移方程,注意对初始k冷冻期间的日期特殊处理
		for (int i = 1; i < prices.size(); i++)
		{
			dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
			if (i <= k)
				dp[i][1] = max(dp[i - 1][1], 0 - prices[i]);
			else
				dp[i][1] = max(dp[i - 1][1], dp[i - k - 1][0] - prices[i]);
		}
		return dp[prices.size() - 1][0];
	}
};

4.第四类股票问题(多次买卖,含手续费)

  • dp[i][0]代表第i+1天没有持股票,dp[i][1]代表第i+1天持有股票(思想为在买入时扣除手续费,当然在卖出时计算也是可行的)
  • 初始化:
    dp[0][0] = 0;         第一天没有持股,这时候相当于没有买入,故为0
    dp[0][1] = 0 - prices[0] - fee;   第一天持股,相当于买入,这时候为 - prices[0] - fee ,本次以买入作为交易的开始,每次买入就扣除手续费,卖出的时候就不扣除手续费了。
  • 状态转移方程:
    没有持股 = max(昨天没有持股今天维持现状 , 昨天持股,今天卖出)【买入扣除手续费,卖出就不扣除了,因为买卖都完成才扣一次手续费】
    dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);
    持股 = max(昨天持股今天维持现状,(昨天没有持股,今天买入))
    dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
class Solution
{
public:
	int maxProfit4_1(vector<int>& prices, int fee)
	{
		if (prices.size() < 2)
			return 0;
		vector<vector<int>> dp(prices.size(), vector<int>(2));
		//2. 0位置的初始化(思想为在买入时扣除手续费)
		dp[0][0] = 0;
		dp[0][1] = 0 - prices[0] - fee;
		//3. 状态转移方程,基本思路与第二种问题类似,多了手续费
		for (int i = 1; i < prices.size(); i++)
		{
			dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
			dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee);
		}
		return dp[prices.size() - 1][0];
	}

	int maxProfit4_2(vector<int>& prices, int fee)
	{
		if (prices.size() < 2)
			return 0;
		vector<vector<int>> dp(prices.size(), vector<int>(2));
		//2. 0位置的初始化(思想为在卖出时扣除手续费)
		dp[0][0] = 0;
		dp[0][1] = 0 - prices[0];
		//3. 状态转移方程,基本思路与第二种问题类似,多了手续费
		for (int i = 1; i < prices.size(); 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]);
		}
		return dp[prices.size() - 1][0];
	}
};

5.第五种股票问题(两次买卖)

  • (这里的交易k次注意,是指已经进行了交易的次数,本题中将买入的时候当做进行了一次交易)
    dp[i][k][0]代表第i+1天交易k次并且没有持股票,dp[i][k][1]代表第i+1天交易k次并且持有股票
    dp[i][k][0]代表第i+1天交易k次并且没有持股票,dp[i][k][1]代表第i+1天交易k次并且持有股票
  • 初始化:(在买入的时候算进行一次交易)
    dp[0][0][0] = 0 //第一天, 交易次数为0,没持股,相当于啥都没干,没有存在意义
    dp[0][1][0] = 0;       //第一天,交易次数为1,没持股,相当于买入再卖出
    dp[0][1][1] = - prices[0]; //第一天,交易次数为1,持股,相当于买入股票未卖出
    dp[0][2][0] = 0;      //第一天, 交易次数为2,没持股,相当于买入卖出买入卖出,没有盈利
    dp[0][2][1] = - prices[0]; //第一天,交易次数为2,持股,相当于买入再卖出(交易1次)再买入
  • 状态转移方程:
    第(i+1)天交易数为2,没持股 = max(前一天交易数为2没持股维持,前一天交易数为2持股今天卖出)
    dp[i][2][0] = max(dp[i-1][2][0],dp[i-1][2][1] + prices[i]);
    第(i+1)天交易数为2,持股 = max(前一天交易数为2持股维持,前一天交易数为1没持股今天买入)[买入之前必须卖出,故之前交易数应该为1而不是为2]
    dp[i][2][1] = max(dp[i-1][2][1],dp[i-1][1][0] - prices[i]);
    第(i+1)天交易数为1,没持股 = max(前一天交易数为1没持股维持,前一天交易数为1持股今天卖出)[卖出之前必须买入,故交易数为1而不是0]
    dp[i][1][0] = max(dp[i-1][1][0],dp[i-1][1][1] + prices[i]);
    第(i+1)天交易数为1,持股 = max(前一天交易数为1持股维持,(没交易,今天买入))
    dp[i][1][1] = max(dp[i-1][1][1],0 - prices[i]);
class Solution
{
public:
	int maxProfit5_1(vector<int>& prices)
	{
		//1. 边界条件
		if (prices.size() < 2)
			return 0;
		vector < vector<vector<int>>> dp(prices.size(), vector<vector<int>>(3, vector<int>(2)));
		//2. 0位置的初始化(针对第一天有无持股,交易次数进行的初始化操作)
		dp[0][0][0] = 0;
		dp[0][1][0] = 0;
		dp[0][1][1] = 0 - prices[0];
		dp[0][2][0] = 0;
		dp[0][2][1] = 0 - prices[0];
		//3. 状态转移方程,穷举了k为1和2时的情况
		for (int i = 1; i < prices.size(); i++)
		{
			//通过下面可见,可以通过循环将k穷举,这里只是单纯的全部写出来了
			dp[i][1][0] = max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i]);
			dp[i][1][1] = max(dp[i - 1][1][1], 0 - prices[i]);
			dp[i][2][0] = max(dp[i - 1][2][0], dp[i - 1][2][1] + prices[i]);
			dp[i][2][1] = max(dp[i - 1][2][1], dp[i - 1][1][0] - prices[i]);
		}
		return dp[prices.size() - 1][2][0];
	}
};

6.第六章股票问题(k次买卖)

  • (这里的交易k次注意,是指已经进行了交易的次数,本题中将买入的时候当做进行了一次交易)
    dp[i][k][0]代表第i+1天已经交易了k次并且没有持股票,dp[i][k][1]代表第i+1天已经交易了k次并且持有股票
  • 初始化:
    dp[0][k][0] = 0; 【k为交易成交的笔数】即第一天,持续的买入卖出买入卖出最后没有留股票,没收益
    dp[0][k][1] = 0 - prices[0]; 【k为交易成交的笔数】即第一天,持续的买入卖出买入最后留了股票,收益为买入的 -prices[0]
  • 状态转移方程:
    第(i+1)天交易数为j,没持股 = max(前一天交易数为j没持股维持,前一天交易数为j持股今天卖出)
    dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
    第(i+1)天交易数为j,持股 = max(前一天交易数为j持股维持,前一天交易数为j-1没持股今天买入)
    dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
  • 关于第六题有个核心问题,在于当k大于了prices大小的一半之后,就没有存在的意义了,所以要处理否则会超时
class Solution
{
public:
	int maxProfit6_1(int k, vector<int>& prices)
	{
		//1. 边界条件
		if (prices.size() < 2 || k <= 0)
			return 0;
		//由于k大小对于数据的影响,这里将根据k的大小进行第二种股票问题加第六种股票问题的实现

		//1.1第六种股票问题实现
		if (k < prices.size() >> 1)
		{
			vector < vector<vector<int>>> dp(prices.size(), vector<vector<int>>(k + 1, vector<int>(2)));
			//2. 0位置的初始化(针对第一天有无持股,交易次数进行的初始化操作)
			for (int i = 0; i <= k; i++)
			{
				dp[0][i][0] = 0;
				dp[0][i][1] = 0 - prices[0];
			}
			//3. 状态转移方程,具体为循环穷举不同k时的情况
			for (int i = 1; i < prices.size(); i++)
			{
				for (int j = 1; j <= k; j++)
				{
					dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
					dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
				}
			}
			return dp[prices.size() - 1][k][0];
		}
		//1.2 当k足够大时,第二种股票问题实现
		else
		{
			vector<vector<int>> dp(prices.size(), vector<int>(2));
			dp[0][0] = 0;
			dp[0][1] = 0 - prices[0];
			for (int i = 1; i < prices.size(); i++)
			{
				dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
				dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
			}
			return dp[prices.size() - 1][0];
		}
	}
};

参考文献:
[1] https://blog.csdn.net/qq_17556191/article/details/95209833
[2] https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
[3] 《剑指offer(第二版)》

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方寸间沧海桑田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值