LeetCode股票交易系列一共有6道题,运用贪心思想和动态规划来解题!
文章目录
题目
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。
条件:在主题干下分别加上以下条件
- LeetCode 121:最多进行 1 笔交易(k=1)【贪心】
- LeetCode 122:不限交易次数【二维 DP】
- LeetCode 309:不限交易次数,但有「冷冻期」的额外条件
- LeetCode 714:不限交易次数,但有「手续费」的额外条件
- LeetCode 123:最多进行 2 笔交易(k=2)【三维 DP】
- LeetCode 188:最多进行 k 次交易
分析
1) 最多进行 1 笔交易(k=1)【贪心算法实现】
思路:循环n天的股票价格;
记录并更新最小价格,已最小价格为买入价;
已当前价格为卖出价,记录并更新最大利润。
方程:参数 i 表示第几天
minPrice = max(price[i],minPrice);
profit = max(price[i]-minPrice,profit);
代码:
public int maxProfit1(int[] prices) {
if (prices.length == 0) {
return 0;
}
int minPrice = prices[0];
int profit = 0;
for (int price : prices) {
minPrice = Math.min(minPrice, price);
profit = Math.max(profit, price - minPrice);
}
return profit;
}
结果:
2) 不限交易次数【二维 DP】
思路:其实就是要把每一个上升波段的钱都要赚到。
思路1:找到每次的极小值和极大值,求和每次极大值减去极小值,即可获得最大利润。// 暂不暂时代码。
思路2:二维DP
每天都有三种动作:买入(buy)、卖出(sell)、无操作(rest)。
因为不限制交易次数,因此交易次数这个因素不影响题目,不必考虑。
DP Table 是二维的,两个维度分别是天数(0,1,…,n-1)和是否持有股票(1 表持有,0 表不持有)。
方程:
设 i 为第几天,于是可以得到动态规划方程:
p[i][0] = max(p[i - 1][0], p[i - 1][1] + price[i]);
p[i][1] = max(p[i - 1][1], p[i - 1][0] - price[i]);
边界条件为:
p[0][0] = 0; // 第一天不持有股票
p[0][1] = -price[0]; // 第一天买入股票
代码:
public int maxProfit2(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int[][] profit = new int[n][2];
profit[0][0] = 0;
profit[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
}
return profit[n - 1][0];
}
结果:
3) 不限交易次数,但有「冷冻期」的额外条件。
解释:卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
思路:间隔一天才能买入,假设第 i 天买入,可推算出第 i-1 天和 i-2 天手上都没有股票。
动态规划方程:
P[i][0] = max(p[i-1][0],p[i-1][1]+price[i]);
P[i][1] = max(p[i-1][1],p[i-2][0]-prices[i]); // i 天能买股票的条件是 i-2 天或之前手上已经卖出了股票。
代码:
public int maxProfit3(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int[][] profit = new int[n][2];
profit[0][0] = 0;
profit[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
profit[i][1] = Math.max(profit[i - 1][1], profit[Math.max(i - 2, 0)][0] - prices[i]);
}
return profit[n - 1][0];
}
结果:
4) 不限交易次数,但有「手续费」的额外条件
解释:每笔交易你只需要为支付一次手续费。
思路:在第二题基础上减去费用就好
动态规划方程:
每次买入时减去一个手续费就好
p[i][0] = max(p[i-1][0],p[i-1][1]+price[i]);
p[i][1] = max(p[i-1][1],p[i-1][0]-prices[i]-fee);
代码:
public int maxProfit4(int[] prices, int fee) {
int n = prices.length;
if (n == 0) {
return 0;
}
int[][] profit = new int[n][2];
profit[0][0] = 0;
profit[0][1] = -prices[0] - fee;
for (int i = 1; i < n; i++) {
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i] - fee);
}
return profit[n - 1][0];
}
结果:
5) 最多进行 2 笔交易(k=2)【三维 DP】
思路:多了交易次数的条件,所以升为三维动态规划
动态规划方程:
假设到第i天已经进行了j次交易,有:
p[i][j][0] = max(p[i-1][j][0],p[i-1][j][1]+price[i]); // 今天没有进行操作,所以次数依然是j
p[i][j][1] = max(p[i-1][j][1],p[i-1][j-1][0]-price[i]); // 今天进行了操作,上一天操作数就是j-1
边界条件为:
当交易次数 j 为0时
p[i][0][0] = 0;
p[i][0][1] = Integer.MIN_VALUE; // 交易次数为0用最小值表示不可能持有
当天数只有一天时
profit[0][j][0] = 0;
profit[0][j][1] = -prices[0]; // 其实这里的j代表了还剩余多少交易次数,j必须>0才有效
代码:
public int maxProfit5(int[] prices) {
int n = prices.length;
int k = 2;
if (n == 0) {
return 0;
}
int[][][] profit = new int[n][k + 1][2];
// 交易次数为0时
for (int i = 0; i < n; i++) {
profit[i][0][0] = 0;
profit[i][0][1] = Integer.MIN_VALUE; // 交易次数为0用最小值表示不可能持有
}
for (int j = 1; j < k + 1; j++) {
profit[0][j][0] = 0;
profit[0][j][1] = -prices[0];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < k + 1; j++) {
profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i]);
profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - 1][j - 1][0] - prices[i]);
}
}
return profit[n - 1][k][0];
}
结果:
6) 最多进行 k 次交易
思路:理论和上题的思路一样,将2换成k就可以了。但是直接提交容易出现超内存的错误,是 DP Table 太大导致的。
结果果然超出内存限制。
推理可知交易次数最多为n/2次,当K>=n/2时可以按照不限交易次数进行处理,可以转化为二维DP方程,降低了内存
于是得到代码:
public int maxProfit6(int k, int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
// 交易次数最多为n/2次,当K>=n/2时按照不限交易次数处理
if (n / 2 < k) {
return maxProfit2(prices); // 调第二题的方法来做
}
int[][][] profit = new int[n][k + 1][2];
for (int j = 1; j < k + 1; j++) {
profit[0][j][0] = 0;
profit[0][j][1] = -prices[0];
}
// 交易次数为0时
for (int i = 0; i < n; i++) {
profit[i][0][0] = 0;
profit[i][0][1] = Integer.MIN_VALUE; // 交易次数为0用最小值表示不可能持有
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < k + 1; j++) {
profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i]);
profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - 1][j - 1][0] - prices[i]);
}
}
return profit[n - 1][k][0];
}
结果:
总代码和测试样例结果:
将上面六个方法都放到下面这个类中并运行main方法
public class Stock {
public static void main(String[] args) {
Stock stock = new Stock();
int[] prices = new int[]{7, 1, 5, 3, 6, 4};
System.out.println(stock.maxProfit1(prices));
System.out.println(stock.maxProfit2(prices));
System.out.println(stock.maxProfit3(prices));
prices = new int[]{1, 3, 2, 8, 4, 9};
System.out.println(stock.maxProfit4(prices, 2));
prices = new int[]{1, 2, 3, 4, 5};
System.out.println(stock.maxProfit5(prices));
prices = new int[]{2, 4, 1};
System.out.println(stock.maxProfit6(2, prices));
}
}
测试运行结果:
5
7
5
8
4
2
本文借鉴:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/gu-piao-jiao-yi-xi-lie-cong-tan-xin-dao-dong-tai-g/