买卖股票最佳时机及衍生题
面试被问到了这系列问题,只能在事后亡羊补牢……
题目一:能多次买卖股票
因为单次买卖的题目比较简单,所以跳过:
“给定一个整数数组prices,其中第i个元素代表了第i天的股票价格。可以无限次地完成交易,但是每次只能进行一次交易。返回获得利润的最大值。”
力扣链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
方法一,暴力搜索法
每个日子(除了第一天)都有两种状态:持仓(之前某天卖了股票)或空仓(持有现金等待机会)。如果持有股票,可以分为继续持有和卖出股票;如果持有现金可以分为继续空仓和买入股票。因此形成一个dfs的树状搜索,但是效率肯定很低。图片取自力扣大神liweiwei1419:
方法二,贪心法超短线交易
以[1, 3, 8, 2, 4, 9]为例,其实可以将每一天都与前一天进行比较,如果得到利润为正就进行交易,否则保持空仓。因此实际进行的就是只有交易时长为1天的超短线交易。
假如数据为[1, 2, 3, 4],根据贪心法会进行3次交易。其结果和第一天买第四天卖是一样的。该算法实现起来也很简单:
int maxProfit(vector<int>& prices) {
if (prices.size() < 2) // 异常输入
return 0;
int sum = 0;
for (int i = 1; i < prices.size(); i++) {
if (prices[i] > prices[i-1]) { // 今天相比昨天涨了,就进行一次交易
sum += (prices[i] - prices[i-1]);
}
}
return sum;
}
方法三,动态规划
先用传统方法实现,每一天有两种状态:空仓和持仓。空仓状态用cash数组表示,持仓用hold数组。cash[i]代表截至第i天,此时为空仓且资金剩余多少;hold[i]同理。最后返回cash[size - 1]即最后一天的持有现金,因为毫无疑问cash[size - 1] > hold[size - 1]。
状态转移方程详见代码:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size < 2) // 异常输入
return 0;
// cash数组代表当前空仓的剩余金额,hold代表当前持仓后的剩余金额
int cash[size], hold[size];
cash[0] = 0; // 初始资金为0
hold[0] = -prices[0]; // 假设第一天持仓
for (int i = 1; i < size; i++) {
// 这个过程还是推荐自己动笔算一下哈
cash[i] = max(cash[i - 1], hold[i - 1] + prices[i]);
hold[i] = max(hold[i - 1], cash[i - 1] - prices[i]);
}
return cash[size - 1];
}
因为实际cash和hold每次都只比较前一天,因此可以不用数字,只用几个变量就可以优化了,具体优化参考下一题。
题目二:能多次买卖股票,带手续费
“给定一个整数数组prices,其中第i个元素代表了第i天的股票价格;非负整数fee代表了交易股票的手续费用。
可以无限次地完成交易,但是每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。”
力扣链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
有手续费的情况,仍然可以使用暴力搜索。但是不能再使用贪心:
假如输入[1, 4, 5, 8],手续费为2,用贪心进行超短线交易,利润为(4 - 1 - 2) + (8 - 5 - 2) = 2。
但实际最优为第一天买入第四天卖出,利润为5。因此只能使用动态规划,优化后的动态规划不再使用数组,因为每一天的cash和hold只取决于前一天的precash和prehold。
优化后的动态规划(滚动数组)
int maxProfit(vector<int>& prices, int fee) {
if (prices.size() < 2) // 异常输入
return 0;
int cash = 0, hold = -prices[0];
int precash = cash, prehold = hold; // 用precash和prehold代替数组
for (int i = 1; i < prices.size(); ++i)
{
cash = max(precash, prehold + prices[i] - fee);
hold = max(prehold, precash - prices[i]);
prehold = hold;
precash = cash;
}
return cash;
}
实际上还可以进一步简化,连precash和prehold都不需要了:
int maxProfit(vector<int>& prices, int fee) {
if (prices.size() < 2) // 异常输入
return 0;
int cash = 0, hold = -prices[0];
for (int i = 1; i < prices.size(); ++i)
{
cash = max(cash, hold + prices[i] - fee);
hold = max(hold, cash - prices[i]);
}
return cash;
}
题目三:最多交易2次股票
力扣链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
动态规划
只能交易两次的话,每一天都有五种可能的状态:
- 从未交易的初始状态,金额为0
- 买入一次股票且未卖出,即第一次买入股票,hold[0][i]
- 第一次卖出股票,cash[0][i]
- 第二次买入股票且未卖出,hold[1][i]
- 第二次卖出股票,cash[1][i]
因为数组初始值设定为0,因此可以忽略状态一,第一次购入股票有:hold[0][i] = -prices[i]。
“hold[0][0] = -prices[0]; ” 很好理解,即假设第一天买入股票。那么为什么还要“hold[1][0] = -prices[0];”?
因为题目规定最多交易2次,但不代表必须交易2次也有可能只交易一次。 如果只交易一次,此时就从第二次买入的状态开始动态规划(即跳过第一次买入hold[0][i],从hold[1][i]开始)。 那么既然是从第二次买入的状态开始,那他初始值也必须是在第一天买下股票不然等于第一天无成本买下了股票。
状态转移详见代码,强烈推荐自行手写一次,提供个较好的例子[3, 5, 0, 3, 1, 4]:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if (n < 2 || k < 1)
return 0;
// cash[0][i]代表第i天已交易了一次,cash[1][i]表示已交易了两次。
vector<vector<int>> cash(k, vector<int>(n, 0));
// hold[0][i]代表第i天持有股票(第一次持仓),cash[1][i]表示第二次持有股票。
// 注意,前一天状态为持有股票不代表一定是前一天买入的!实际是从最低点买入的。
vector<vector<int>> hold(k, vector<int>(n, 0));
hold[0][0] = -prices[0];
hold[1][0] = -prices[0];
for(int i = 1; i < n; ++i)
{
cash[0][i] = max(cash[0][i - 1], hold[0][i - 1] + prices[i]);
// hold[0][i]是第一次购买股票,初始金额为0,因此买入就直接取-price[i]。
hold[0][i] = max(hold[0][i - 1], -prices[i]);
// cash[1][i]是第二次卖出。
cash[1][i] = max(cash[1][i - 1], hold[1][i - 1] + prices[i]);
hold[1][i] = max(hold[1][i - 1], cash[0][i - 1] - prices[i]);
}
return cash[1][n - 1];
}
优化后的动态规划(滚动数组)
和之前的多次交易带手续费一样,用滚动数组进行优化:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2)
return 0;
// cash1代表前一天已交易了一次(现在空仓),cash2表示前一天已交易了两次。
int cash1, cash2;
// hold1代表前一天持有股票(第一次持仓),cash2表示前一天第二次持有股票。
// 注意,前一天状态为持有股票不代表一定是前一天买入的!实际是从最低点买入的。
int hold1, hold2;
cash1 = cash2 = 0;
hold1 = hold2 = -prices[0];
for(int i = 1; i < n; ++i)
{
// 今天的cash和hold的状态都通过前一天计算得出
cash1 = max(cash1, hold1 + prices[i]);
hold1 = max(hold1, -prices[i]);
cash2 = max(cash2, hold2 + prices[i]);
hold2 = max(hold2, cash1 - prices[i]);
}
return cash2;
}
题目四:股票交易含冷冻期
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
力扣链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown
这道题中,每一天有三种可能的状态:持有股票、未持有股票且非冷冻期、未持有股票且为冷冻期。
这里引用力扣大神zerotac的讲解:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size < 2)
return 0;
vector<vector<int>> v(size, vector<int>(3));
v[0][0] = -prices[0];
v[0][1] = v[0][2] = 0;
for (int i = 1; i < size; i++)
{
// v[i][0]是第i天持有股票的最大收益,可以是继承前一天状态或前一天(非冷冻期)买了股票。
// v[i][1]是第i天没持有股票且非冷冻期,可以是继承前一天状态或前一天是冷冻期。
// v[i][2]是第i天没持有股票且处于冷冻期,只能是前一天卖了股票。
v[i][0] = max(v[i - 1][0], v[i - 1][1] - prices[i]);
v[i][1] = max(v[i - 1][1], v[i - 1][2]);
v[i][2] = v[i - 1][0] + prices[i];
}
return max(v[size - 1][1], v[size - 1][2]);
}
};