导读
本系列一共分6道题,分别是
Leetcode题号 | 题目 |
---|---|
121 | 买卖股票的最佳时机 |
122 | 买卖股票的最佳时机 II |
123 | 买卖股票的最佳时机 III |
188 | 买卖股票的最佳时机 IV |
309 | 最佳买卖股票时机含冷冻期 |
714 | 买卖股票的最佳时机含手续费 |
以上算法均是用动态规划的思想求解,下面分别看每个题是如何解决的。
通用模板:
状态数组dp[i][k][0,1]:即第i天,最多允许交易k次,当前持有[1]或不持有[0]所获得的最大利润
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
即第i天不持有,两种情况:本来第i-1天就不持有,直接把状态带过来了;或者第i-1天持有,在第i天以prices[i]的价格卖掉了
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
即第i天持有,两种情况:本来第i-1天就持有,第i天没卖,或第i-1天不持有,在第i天以prices[i]的价格买入
1.买卖股票的最佳时机Leetcode121
动态规划(DP method)
因为题目有一个时间顺序,即卖必须是在买之后发生,故每遍历一个新的价格,更新最小股票价格min_price和最大利润max_profile,若用动态规划思想的话,即定义两个数组min_pricei和max_profilei
class Solution {
public:
int maxProfit(vector<int>& prices) {
int min_price = INT_MAX; // 最小价格初始为无穷大
int max_profile = 0; // 最大利润初始为0
for(int i = 0; i < prices.size(); i++) {
min_price = min(min_price, prices[i]); // 每次都要记录最低价格
max_profile = max(max_profile, prices[i] - min_price); //
}
return max_profile;
}
};
2.买卖股票的最佳时机 II / 能进行无数次的股票交易LeetCode122
难度:简单Easy
在本系列模板上直接做修改,
d
p
[
i
]
[
k
]
[
0
,
1
]
dp[i][k][0,1]
dp[i][k][0,1]由于不考虑交易次数,故中间一维k可以忽略,变为
d
p
[
i
]
[
0
,
1
]
dp[i][0,1]
dp[i][0,1]
初始状态:
dp[0][0] = dp[i][0] = 0
dp[0][1] = dp[i][1] = -prices[0]
状态转移方程: 当k=∞时,则可认为此时k和k-1是一样的,故可以把其忽略
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i]) = max(dp[i-1][1], dp[i-1][0] - prices[i])
即状态方程为i行2列的一个二维状态数组,如下:
第0天不持有 | 第0天持有 |
---|---|
第1天不持有 | 第1天持有 |
第2天不持有 | 第2天持有 |
…… | …… |
第i天不持有 | 第i天持有 |
对于第i天持有/不持有,只看前一天持有/不持有的情况,故可以定义两个量preNonHold和preHold,来记录前一天持有/不持有的情况
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length; // 养成好习惯
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// 初始状态
dp[0][0] = 0; // 第0天不买入
dp[0][1] = -prices[0]; // 第0天买入
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0]; // 返回的肯定是手里不持有的情况
}
}
简单方法:
定义一个变量tmp,当某天的价格比前一天高时,就计算差值并加到tmp里,小于就不计算。
class Solution {
public:
int maxProfit(vector<int> &prices) {
int res = 0;
for (int i = 1; i < prices.size(); ++i)
res += max(prices[i] - prices[i - 1], 0); // 比前一天小的情况就不算了
return res;
}
};
3.买卖股票的最佳时机 III / 只能进行两次的股票交易LeetCode123
难度:困难Hard
还是在本系列通用模板上进行修改,状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
一共涉及到两次交易: k=1或k=2,以下两种情况均是
dp[0][0] = 0: 第1天不持有;
dp[0][1] = -prices[0]: 第1天持有;
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
即之前不持有,或者前一天持有,当天卖掉,完成第k次交易.
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
即之前持有,或者之前不持有,当前买入,开启新的一次交易
注意买了再卖算同一次交易(一次完整的交易),卖了在买,买的时候算下一次交易了(k须+1)
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) return 0;
int n = prices.length;
int[][][] dp = new int[n][3][2]; // k的第0维没什么用
dp[0][1][0] = 0; // 第一次交易,无持有,第[0]天肯定为0
dp[0][1][1] = -prices[0]; // 第一次交易,持有(买入第一天的股票)
dp[0][2][0] = 0; // 第二次交易,无持有,第[0]天肯定为0
dp[0][2][1] = -prices[0]; // 第一次交易,持有(买入第一天的股票)(不合逻辑)
for (int i = 1; i < n; i++) {
dp[i][1][0] = Math.max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
// 第1次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第1次交易;
dp[i][1][1] = Math.max(dp[i-1][1][1], -prices[i]);
// 第1次交易,持有: 要么前一天就持有,要么买入一只股票,开启第1次交易;
dp[i][2][0] = Math.max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
// 第2次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第2次交易;
dp[i][2][1] = Math.max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);
// 第2次交易,持有: 要么前一天就持有,要么前一天没持有,买入当前的股票,开启第2次交易
}
return dp[n-1][2][0]; // 第[2]维为0代表完成第k次交易了
}
}
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 特判
if prices is None:
return 0
n = len(prices)
# 创建三维数组: 第[0]维是天数,第[1]维是交易次数(大小为3,第1个元素没用),第[2]维是是否持有(大小为2)
# 注意python定义三维数组的时候有坑, 先定义第[2]维,再定义第[1]维,最后定义第[0]维
dp = [[[0 for i in range(2)] for j in range(3) ] for k in range(n)]
dp[0][1][0] = 0; # 第一次交易,无持有,第[0]天肯定为0
dp[0][1][1] = -prices[0]; # 第一次交易,持有(买入第一天的股票)
dp[0][2][0] = 0; # 第二次交易,无持有,第[0]天肯定为0
dp[0][2][1] = -prices[0]; # 第一次交易,持有(买入第一天的股票)(不合逻辑)
for i in range(1, n):
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
# 第1次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第1次交易;
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
# 第1次交易,持有: 要么前一天就持有,要么买入一只股票,开启第1次交易;
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
# 第2次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第2次交易;
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
# 第2次交易,持有: 要么前一天就持有,要么前一天没持有,买入当前的股票,开启第2次交易
return dp[n - 1][2][0] # 第[2]维为0代表完成第k次交易了
4.买卖股票的最佳时机 IV / 只能进行 k 次的股票交易LeetCode188
难度:困难Hard
idea: 动态规划(DP method)
状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[0][k][0] = dp[i][0][0] = dp[i][0][1] = 0
dp[0][k][1] = -prices[0]
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k = any integer时,上面的状态转移方程没有可简化的地方了
也不能像上一题一样枚举k,故此时只能遍历k
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length == 0) return 0;
int n = prices.length;
int max_k = k;
if (max_k >= n / 2) { // 相当于可以交易无限次了
int res = 0;
for (int i = 1; i < n; i++) {
int value = prices[i] - prices[i - 1];
if (value > 0) res += value;
}
return res;
}
int[][][] dp = new int[n][max_k + 1][2];
// 赋初值,无论第几次的交易在第[0]天持有为"-prices[0]";无持有为"0"
for (int kk = 0; kk <= max_k; kk++) {
dp[0][kk][0] = 0;
}
for (int kk = 0; kk <= max_k; kk++) {
dp[0][kk][1] = -prices[0];
}
// 令i=1,kk=1时,代入下式发现有3个值需要初始化: dp[0][0][0],dp[0][1][1],dp[0][1][0]
for (int i = 1; i < n; i++) {
for (int kk = 1; kk <= max_k; kk++) { // 一次交易至少需要两天,故说有效的限制k不超过n/2
dp[i][kk][0] = Math.max(dp[i-1][kk][0], dp[i-1][kk][1] + prices[i]);
dp[i][kk][1] = Math.max(dp[i-1][kk][1], dp[i-1][kk-1][0] - prices[i]);
}
}
return dp[n-1][max_k][0];
}
}
5.最佳买卖股票时机含冷冻期 / 需要冷却期的股票交易LeetCode309
idea: 动态规划(DP method)
状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k=∞, 并且含有冷冻期,即每次sell之后要等一天才能继续交易
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-2][k][0] - prices[i])
注:第[i]天buy的时候,上一次交易需要在[i-2]天卖,而不是i-1,因为第[i-1]天卖了不能在第[i]天买
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0 || prices.length == 1) return 0;
int n = prices.length;
int[][] dp = new int[n][2];
// 因为下面的for循环需要从i=2开始,所以需要赋4个初值
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]);
dp[1][1] = Math.max(-prices[0], -prices[1]);
for (int i = 2; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
}
return dp[n - 1][0];
}
}
6.买卖股票的最佳时机含手续费 / 需要交易费用的股票交易LeetCode714
idea: 动态规划(DP method) 状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k=∞时,则可认为此时k和k-1是一样的,又因每次交易要支付手续费,故只要把手续费从利润中减去即可
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i] - fee)
class Solution {
public int maxProfit(int[] prices, int fee) {
// 特判
if (prices.length == 0) return 0;
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee; // 初始也要减手续费
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
}
return dp[n - 1][0];
}
}