文章目录
1. 简介
股票问题是
L
e
e
t
C
o
d
e
{\rm LeetCode}
LeetCode中的经典问题,常涉及到使用贪心或动态规划等方法解决。本文将介绍各种类型的股票问题,包括买卖股票的最佳时机Ⅰ Ⅱ Ⅲ Ⅳ
、含冷冻期和含手续费。首先介绍这类问题的基本要求,给定一个表示价格的数组,数组中的元素表示对应日期的股票价格,并且不能同时参与多笔交易(必须在再次购买股票之前出售掉之前的股票)。
2. 买卖股票的最佳时机
题目来源 121.买卖股票的最佳时机
题目描述 最多只允许完成一笔交易(即买入和卖出股票一次),计算所能获得的最大利润。
如输入为[7,1,5,3,6,4]
,则返回结果为5
。因为在第二天买入为1
,第五天卖出为6
时可获得最大利润为5
。利润的计算公式为卖出价格减去买入价格,直观上,如果被减数越大、减数越小,则得到的结果越大。则我们可以在遍历数组时维护前面的最小值和当前值和最小值产生的利润,则可以实现一次遍历得到最终结果。程序如下:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size <= 1) return 0;
int minPrice = prices[0]; // 初始化前面的最小值为第一个元素
int maxRes = 0; // 初始化最大利润为零
for (int i = 1; i < size; ++i) {
maxRes = max(maxRes, prices[i] - minPrice); // 计算当前值和最小值的利润
minPirce = min(minPrice, prices[i]); // 是否更新前面的最小值
}
return maxRes;
}
其他题解 官方题解
3. 买卖股票的最佳时机Ⅱ
题目来源 122.买卖股票的最佳时机Ⅱ
题目描述 与上一题不同的是,该题要求你可以完成多笔交易(多次买卖股票),但同时也需要满足不能同时参与多笔交易的基本要求。
如输入为[7,1,5,3,6,4]
,则返回结果为7
。因为在第二天买入为1
,第三天卖出为5
时的利润为4
;在第四天买入为3
,第五天卖出为6
时的利润为3
。
由于不能同时参与多笔交易,所以每天交易结束后只可能存在两个状态,即手里有股票和手里没有股票。由于需要记录每一天以及可能存在的两种状态,我们使用dp[i][0]
表示在第i
天交易结束后手里没有股票时得到的最大利润,dp[i][1]
表示在第i
天交易结束后手里有股票时得到的最大利润。
先看dp[i][0]
,当天交易结束后手里没有股票又存在两种情况:前一天没有股票,当天没有任何操作,使用dp[i - 1][0]
表示;前一天有股票,当天将其卖出,使用dp[i - 1][1] + prices[i]
表示(这里使用加法表示当天获得的受益,后面在初始化的时候会提到)。
再看dp[i][1]
,当天交易结束后手里有股票又存在两种情况:前一天没有股票,当天买入股票,使用dp[i - 1][0] - prices[i]
(同上);前一天有股票,当天没有任何操作,使用dp[i - 1][1]
表示。
对于数组的初始化,第一项dp[0][0]
表示第一天结束后手里没有股票,即利润为零;第二项dp[0][1]
表示第一天结束后手里有股票,即利润为-prices[0]
。此外,由于全部交易结束后,手里有股票时的利润一定低于手里没有股票时的利润,因此最后返回dp[size - 1][0]
即可。程序如下:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size <= 1) return 0;
vector<vector<int>> dp(size, vector<int>(2));
dp[0][0] = 0, dp[0][1] = -prices[0];
for (int i = 1; i < size; ++i) {
// 判断是否更新当天状态
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
}
return dp[size - 1][0];
}
贪心法 由于股票获得利润的规则是后面天卖出的价格减去前面天买入的价格,则总利润等价于寻找 x x x个不相交的区间 ( l i , r i ] (l_i,r_i] (li,ri]使得下式最大化: ∑ i = 1 x p r i c e s [ r i ] − p r i c e s [ l i ] (1) \sum_{i=1}^xprices[r_i]-prices[l_i]\tag{1} i=1∑xprices[ri]−prices[li](1)
而区间 ( l i , r i ] (l_i,r_i] (li,ri]的贡献价值 p r i c e s [ l i ] − p r i c e s [ r i ] prices[l_i]-prices[r_i] prices[li]−prices[ri]等价于若干个长度为一的区间 ( l i , l i + 1 ] , ( l i + 1 , l i + 2 ] , . . . , ( r i − 1 , r i ] (l_i,l_i+1], (l_i+1,l_i+2],...,(r_i-1,r_i] (li,li+1],(li+1,li+2],...,(ri−1,ri]的贡献价值总和。因此上述问题可以简化为找到 x x x个长度为一的区间 ( l i , l i + 1 ] (l_i,l_i+1] (li,li+1]使得式 ( 1 ) (1) (1)最大。使用贪心考虑每次选择贡献大于零的区间即能得到最后结果的最大化,因此最后答案可表示为: m a x R e s = ∑ i = 1 s i z e − 1 max { 0 , p r i c e s [ i ] − p r i c e s [ i − 1 ] } (2) maxRes = \sum_{i=1}^{size-1}\max\left\{0,\ prices[i]-prices[i-1]\right\}\tag{2} maxRes=i=1∑size−1max{0, prices[i]−prices[i−1]}(2)
程序如下:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size <= 1) return 0;
int maxRes = 0;
for (int i = 1; i < size; ++i)
maxRes += max(0, prices[i] - prices[i - 1]);
return maxRes;
}
其他题解 官方题解
4. 买卖股票的最佳时机Ⅲ
题目来源 123.买卖股票的最佳时机Ⅲ
题目描述 与上一题不同的是,该题要求你最多可以完成两笔交易(两次买卖股票),但同时也需要满足不能同时参与多笔交易的基本要求。
该题与上一题不同的是限制了总的买卖次数不超过两次,也就是说最后得到的利润要么来自于一次买卖股票,要么来自于两次购买股票。所以,在上一道题的基础上,我们使用的动态规划数组需要多增加一维来表示当天已经完成的股票买卖次数。根据上一题动态规划数组的含义,我们可以定义如下三维数组的含义。
dp[i][0][0]
表示当天交易结束后手里没有股票,且买卖次数为零。则易得此时的利润为零,即dp[i][0][0] = 0
。
dp[i][0][1]
表示当天交易结束后手里没有股票,且买卖次数为一。则此时可能存在两种情况:当天将股票卖了,使得买卖次数加一,使用dp[i - 1][1][0] + prices[i]
表示;当天以前卖的股票,且今天没有购买股票,使用dp[i - 1][0][1]
表示。
dp[i][0][2]
表示当前交易结束后手里没有股票,且买卖次数为二。则此时可能存在两种情况:当前将股票卖了,使得买卖次数加一,使用dp[i - 1][1][1] + prices[i]
表示;当天以前卖的股票,且今天没有购买股票,使用dp[i - 1][0][2]
表示。
dp[i][1][0]
表示当天交易结束后手里持有股票,且买卖次数为零。则此时可能存在两种情况:当天购买的股票,使用dp[i - 1][0][0] - prices[i]
表示;当天以前购买的股票,使用dp[i - 1][1][0]
表示。
dp[i][1][1]
表示当天交易结束后手里持有股票,且买卖次数为一。则此时可能存在两种情况:当天购买的股票,使用dp[i - 1][0][1] - prices[i]
表示;当天以前购买的股票,使用dp[i - 1][1][1]
表示。
dp[i][1][2]
表示当前交易结束后手里持有股票,且买卖次数为二。这种情况不存在,因为题目限制了最多只能完成两笔交易。我们设置为dp[i][1][2] = INT_MIN / 2
(不设置为INT_MIN
防止减法溢出)。
对于数组的初始化,第一项dp[0][0][0]
表示什么也没有发生,即dp[0][0][0] = 0
;第二项dp[0][0][1]
表示第一天结束后已经完成了一笔交易,显然不可能出现此情况,即dp[0][0][1] = INT_MIN / 2
;第三项,同第二项,即dp[0][0][2] = INT_MIN / 2
;第四项dp[0][1][0]
表示第一天结束后手里持股,则第一天购买了股票,即dp[0][1][0] = -prices[i]
;第五项dp[0][1][1]
,同第二项,即dp[0][1][1] = INT_MIN / 2
;第六项dp[0][1][2]
,同第三项,即dp[0][1][2] = INT_MIN / 2
。交易结束后,最大利润来源于一次交易或两次交易且手里没有票,即dp[size - 1][0][1]
或dp[size - 1][0][2]
,且期间如果没有参与股票的购买和出售,则利润为零。程序如下:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size <= 1) return 0;
// 定义三维动态规划数组,数组具体含义见上
vector<vector<vector<int>>> dp(size, vector<vector<int>>(2, vector<int>(3, 0)));
// 初始化
dp[0][0][0] = 0;
dp[0][0][1] = INT_MIN / 2;
dp[0][0][2] = INT_MIN / 2;
dp[0][1][0] = -prices[0];
dp[0][1][1] = INT_MIN / 2;
dp[0][1][2] = INT_MIN / 2;
// 遍历数组
for (int i = 1; i < size; ++i) {
// 当前结束后,当前未持股,则该数组项始终为零
dp[i][0][0] = 0;
// 根据上述讲解更新其他五个变量的值
dp[i][0][1] = max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
dp[i][0][2] = max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
dp[i][1][0] = max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
dp[i][1][1] = max(dp[i - 1][0][1] - prices[i], dp[i - 1][1][1]);
dp[i][1][2] = INT_MIN / 2;
}
// 返回
return max(0, max(dp[size - 1][0][1], dp[size - 1][0][2]));
}
5. 买卖股票的最佳时机Ⅳ
题目来源 188.买卖股票的最佳时机Ⅳ
题目描述 与上一题不同的是,该题要求你最多可以完成k笔交易(k次买卖股票),但同时也需要满足不能同时参与多笔交易的基本要求。
当然,这一题的解法可以与上一题完全一致,即将动态规划数组大小设置成dp[size][1][k + 1]
且含义类似。对于初始化和上一题类似,dp[0][0][0] = 0
以及dp[0][1][0] = -prices[i]
,其余变量均初始化为INT_MIN / 2
。首先来看上面解答中的循环体:
// k = 2的情况
dp[i][0][0] = 0; // 由于初始化vector的时候所有项均为零,这一项可有可无
dp[i][0][1] = max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
dp[i][0][2] = max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
dp[i][1][0] = max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
dp[i][1][1] = max(dp[i - 1][0][1] - prices[i], dp[i - 1][1][1]);
dp[i][1][2] = INT_MIN / 2; // 当j=k这一项单独处理
// j为任意值的情况,且1 <= j <= k
dp[i][0][j] = max(dp[i - 1][1][j - 1] + prices[i], dp[i - 1][0][j]);
dp[i][1][j] = max(dp[i - 1][0][j] - prices[i], dp[i - 1][1][j]);
// 特殊项
dp[i][1][0] = max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
dp[i][1][k] = INT_MIN / 2;
最后,在返回结果的时候重新遍历一次动态规划数组,判断在完成几笔交易时可以获得最大利润。程序如下:
int maxProfit(int k, vector<int>& prices) {
if (k == 0) return 0;
int size = prices.size();
if (size <= 1) return 0;
// 定义三维动态规划数组,数组具体含义见上
vector<vector<vector<int>>> dp(size, vector<vector<int>>(2, vector<int>(k + 1, 0)));
// 初始化,根据分析dp[0][0][1]往后和dp[0][1][1]往后均为INT_MIN / 2
dp[0][0][0] = 0;
for (int i = 1; i <= k; ++i)
dp[0][0][i] = INT_MIN / 2;
dp[0][1][0] = -prices[0];
for (int i = 1; i <= k; ++i)
dp[0][1][i] = INT_MIN / 2;
// 遍历数组
for (int i = 1; i < size; ++i) {
// 按照上述规则更新动态规划数组
dp[i][1][0] = max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
for (int j = 1; j <= k; ++j) {
dp[i][0][j] = max(dp[i - 1][1][j - 1] + prices[i], dp[i - 1][0][j]);
dp[i][1][j] = max(dp[i - 1][0][j] - prices[i], dp[i - 1][1][j]);
if (j == k) dp[i][1][k] = INT_MIN / 2;
}
}
// 查看交易几次能够获得最大利润
int result = 0;
for (int i = 1; i <= k; ++i) {
result = max(result, dp[size - 1][0][i]);
}
// 返回
return result;
}
6. 最佳买卖股票时机含冷冻期
题目来源 309.最佳买卖股票时机含冷冻期
题目描述 与上一题不同的是,该题要求你可以完成多笔交易(多次买卖股票),但同时也需要满足不能同时参与多笔交易的基本要求。并且,在卖出股票后的第二天无法购买股票(即含有一天的冷冻期)。
参考上面第三个问题的解法,由于不能同时参与多笔交易,所以每天交易结束后只可能存在两个状态,即手里有股票和手里没有股票。与上面定义的数组类似,但这里又存在一个冷冻期的情况。所以,最终的动态规划数组定义如下:使用dp[i][0]
表示在第i
天交易结束后手里有股票时得到的最大利润,dp[i][1]
表示当天交易结束后手里没有股票,且不处于冷冻期;使用dp[i][2]
表示当天交易结束后手里没有股票,且处于冷冻期。
先看dp[i][0]
,当天交易结束后手里有股票又存在两种情况:前一天没有股票,当天买入则表示前一天不能处于冷冻期,使用dp[i - 1][1] - prices[i]
表示;前一天有股票,使用dp[i - 1][0]
表示。
再看dp[i][1]
,当天交易结束后手里没有股票且不处于冷冻期,此时又存在两种情况:前一天处于冷冻期导致前一天不能购买股票,使用dp[i - 1][2]
表示;前一天手里没有股票,当天也没有购买股票,使用dp[i - 1][1]
表示。
再看dp[i][2]
,当天交易结束后手里没有股票且处于冷冻期,由于当天交易结束后处于冷冻期,说明在当天将股票卖出,使用dp[i - 1][0] + prices[i]
。
对于数组的初始化,第一项dp[0][0]
表示第一天结束后手里有股票,即利润为-prices[0]
;第二项dp[0][1]
表示第一天结束后手里没有股票且不处于冷冻期,即利润为零;第三项dp[0][1]
表示第一天结束后手里没有股票且处于冷冻期(不可能出现这种情况),即利润为零。此外,由于全部交易结束后,手里有股票时的利润一定低于手里没有股票时的利润,因此最后的返回结果在dp[size - 1][1]
和dp[size - 1][2]
中取得。程序如下:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if (size <= 1) return 0;
// 定义动态规划数组
vector<vector<int>> dp(size, vector<int>(3, 0));
// 初始化
dp[0][0] = -prices[0];
// 遍历
for (int i = 1; i < size; ++i) {
dp[i][0] = max(dp[i - 1][1] - prices[i], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][2], dp[i - 1][1]);
dp[i][2] = dp[i - 1][0] + prices[i];
}
// 返回
return max(dp[size - 1][1], dp[size - 1][2]);
}
其他题解 官方题解
7. 买卖股票的最佳时机含手续费
题目来源 714.买卖股票的最佳时机含手续费
题目描述 与上一题不同的是,该题要求你可以完成多笔交易(多次买卖股票),但同时也需要满足不能同时参与多笔交易的基本要求。并且,每笔交易都需要付手续费。这里的交易是指买入,持有并卖出股票整个过程。
这个问题的求解其实是与上面第三个问题的解法一致,只是在卖出股票时需要加上手续费的支出。我们先来看上面问题的循环体部分:
// 当天结束后不持股,后面第一项为前一天未持股、第二项为前一天持股,当天卖出则需要产生手续费
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
// 当天结束后持股,后面第一项为前一天未持股、第二项为前一天持股
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
// 则上述转移方程变成
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
其他的内容与上面第三个问题一致。程序如下:
int maxProfit(vector<int>& prices, int fee) {
int size = prices.size();
if (size <= 1) return 0;
vector<vector<int>> dp(size, vector<int>(2));
dp[0][0] = 0, dp[0][1] = -prices[0];
for (int i = 1; i < size; ++i) {
// 判断是否更新当天状态
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
}
return dp[size - 1][0];
}
其他题解 官方题解
8. 股票问题总结
股票问题是动态规划中的经典题型,其中动态规划数组的定义对于使用动态规划的方法解决其他问题有重大借鉴意义,特别是上面的第四个问题和第六个问题。最后,上述问题的解法可能不是最优的,即有的题目的解答存在可以优化空间的情况。
参考
- https://leetcode-cn.com/problemset/all/.
- https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/tong-su-yi-dong-de-dong-tai-gui-hua-jie-fa-by-marc/.(4思路)