股票问题就是分清有几个状态,然后弄清每个状态是由哪个状态转化而来的。
其实和背包问题很类似,背包问题是 物品装与不装,股票就是买与不买。不同点在于,股票在买与不买的基础上,还有卖与不卖的另一种状态!
无操作=当日买入+当日再卖出
代码随想录中dp包含5种状态,他是4种状态+1种操作(无操作)。
我这里给出和之前121题、122题背包问题,一致的解析,即dp存储的是股票持有的状态,一共4种。
dp(4)
dp[i][0]= max(dp[i - 1][0], -prices[i]);
// 注意第一次买入,不能加历史记录!
/**
* @Date: 2023 Dec 27
* @Time: 08:08
* @Author: Chris
* @Title: 123. Best Time to Buy and Sell Stock III
* @Link:
*https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/
**/
#include
#include
#include
using namespace std;
class Solution {
// at most two transactions
// mush sell the stock before buy again
public:
// 状态分析:
// 0. 第一次持有股票
// 1. 第一次不持有
// 2. 第二次持有股票
// 3. 第二次不持有
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(4));
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = -prices[0];
dp[0][3] = 0;
for (int i = 1; i < prices.size(); ++i) {
// 第一次持有 = 第一次保持持有| i-1不持有 今日买入。
// 注意第一次买入,不能加历史记录!和121题一致
dp[i][0] = max(dp[i - 1][0], -prices[i]);
// 第一次不持有= 第一次保持不持有| i-1持有,今日卖出
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
// 第二次持有 = 第二次保持持有| i-1第一次不持有,今日买入
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
// 第二次不持有 = 保持第二次不持有|第二次持有+今日卖出
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
}
return dp.back()[3];
}
};
其实还有一个问题要清楚,买与卖的操作,持有不持有的状态,只是便于我们理解,dp也可以表示为操作,
0.无操作 1.第一次买入 2.第一次卖出 3.第二次买入 4.第二次卖出。
哪种方式能让你容易理解,你就用哪种去理解,最后公式结果都是一样的。
因为我们只是用两行数据第 i 行和 i-1 行,所以可以用2行的滚动数组存储,2行还可以压缩为1行。为什么? 因为for循环(假设从前向后遍历)每个元素,更新一个元素时,其后面的元素就是上一行的历史数据。从后向前遍历,同理。
class Solution {
public:
// 滚动数组空间优化
int maxProfit(vector<int>& prices) {
// 状态分析:
// 0. 第一次持有股票
// 1. 第一次不持有
// 2. 第二次持有股票
// 3. 第二次不持有
vector<int> dp(4, 0);
dp[0] = -prices[0];
dp[2] = -prices[0];
for (int i = 1; i < prices.size(); ++i) {
// dp[0]是求负数的最大值=求正数最小值,其实就是当前序列中最便宜的股票
dp[0] = max(-prices[i], dp[0]);
dp[1] = max(dp[0] + prices[i], dp[1]);
dp[2] = max(dp[1] - prices[i], dp[2]);
dp[3] = max(dp[2] + prices[i], dp[3]);
}
return dp.back();
}
};
注意事项:
dp[0] = max(-prices[i], dp[0]);
dp[1] = max(dp[0] + prices[i], dp[1]);
看第二行代码,左侧dp[1]是当前第i天的第一次不持有状态, max函数中最右侧dp[1]是历史数据,dp[0] 而是第 i 天的最新的数据,因为dp[0]在第一行代码中先更新了。
为什么用当天最新的数据也可以?
那要明白dp[0]中存储的是什么?
dp[0] = max(-prices[i], dp[0]);
可以看出是股票历史最低价,不过是负数存储。
历史dp[0]存储的是 从第1天到第i-1天 股票最低价格,当日dp[0] 就多考虑了1天。
那么我们分析多考虑一天会不会对我们dp[1]数据造成影响?
因为dp[0]存储的是历史最低价,
所以看第i天是不是历史最低价?
a.是→分析影响,
b.不是→没影响不用管。因为不是的话,dp值没更新,当日dp[0] = 历史dp[0],
是的话,收益为0。
dp[0] = -prices[i]; dp[0] + prices[i] == 0,
对dp[1]第一次卖出没影响的,因为第一次卖出dp[1]的全局最小值就是0,即当天买当天卖,其他时候我们所执行的操作都收益大于0的。
综上,分析思路:先找到区别点在哪里?再弄清区别具体是什么?
如果num[i]是历史最低,第一次持有dp[0]才会更新,但dp[0]+price[i]==0, 对dp[1]第一次卖出求最大值无影响。
同理 dp[1]没影响的话,dp[2]也就无影响,dp[3]也无影响。
k次交易,dp包含2 * k个状态,
注意:第i天第1次持有是dp[i][1]=-prices[i]
,而其他第2、3、4…次持有都是带历史收益的dp[i][1]=dp[i-1][1] - prices[i]
为了使代码保持一致,多出第0次状态。dp[i][0]=0,。
dp[i][1]=dp[i-1][1] - prices[i]
/**
* @Date: 2023 Dec 27
* @Time: 22:17
* @Author: Chris
* @Title: 188. Best Time to Buy and Sell Stock IV
* @Link:
*https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/
**/
#include
#include
using namespace std;
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
// j奇数为买入,减去第一天股票价格
for (int j = 1; j < 2 * k + 1; j += 2) {
dp[0][j] = -prices[0];
}
// j = 0. 无操作,值为0,便于统一第一次买入操作
// = 1. 第一次持有
// = 2. 第一次不持有
// = 3. 第二次持有
// = 4. 第二次不持有
// = 5................
// 从第二天开始
for (int i = 1; i < prices.size(); ++i) {
// 从j=1开始 一次计算2个状态,比如1,2; 3,4; 5,6;
for (int j = 1; j < 2 * k; j += 2) {
// 第i天持有 = i-1天不持有,今日买入 | 与i-1天的持有状态保持一致
dp[i][j] = max(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]);
// 第i天不持有 = i-1持有,今日卖出 | 与i-1天的不持有状态保持一致
dp[i][j + 1] = max(dp[i - 1][j] + prices[i], dp[i - 1][j + 1]);
}
}
return dp.back().back();
}
};
int main() { return 0; }