Leetcode
上有一个买卖股票系列的算法问题,主要区别在于是否有交易次数限制、是否交易有冷却期、是否有交易手续费等条件。本文探究的就是这个系列的通用思路和解法、不同条件时的修改以及最优解。阅读本文需要事先对这个系列各个问题的题目有一定的了解,了解 动态规划
。本文会从最复杂的条件开始,得出最通用的解法,所以一开始反而是最难的,推荐有兴趣、有耐心的读者先从头到尾阅读一遍,如果难以理解的话,再从最简单的条件开始阅读,这样就可以深刻了解这个系列的解题思路,掌握解题模板。本文代码使用的语言是 Java
。
核心思路
定义一个二维数组 int[][] profit = new int[n][2]
,其中 profit[i][0]
表示第 i
天卖出股票(没有持有股票)时的最大收益,profit[i][1]
表示表示第 i
天买入股票(持有股票)时的最大收益
那么状态转移方程是:
// 第i天的卖出状态 = Max(前一天卖出状态,前一天买入状态 + 卖出股票获得的收益)
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
// 第i天的买入状态 = Max(前一天买入状态,前一天前一次已卖出状态 - 买入股票扣除的收益)
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
这个系列所有问题的解答都是基于这个状态转移方程
通用的解法
首先得出考虑了 Leetcode
上买卖股票系列所有不同条件下的通用解
// k表示交易次数,fee表示交易手续费,m表示交易冷却期
public int maxProfit(int k, int[] prices, int fee, int m) {
if (k == 0 || prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
// 进行一次完全的交易需要两天,所以当 k > n/2 的时候,就可以每天都进行一次买入(卖出)操作,也就是可以交易无数次
if (k > (n >> 1)) {
int[][] profit = new int[n][2];
for (int i = 0; i < n; i++) {
// 处理初始状态
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
// 处理有交易冷却期时,前 m + 1 天的情况,由于这时候不能卖出,所以前一天卖出状态的最大收益都为0
if (i < m + 1) {
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
profit[i][1] = Math.max(profit[i - 1][1], 0 - prices[i]);
continue;
}
// 核心,状态转移方程
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
profit[i][1] = Math.max(profit[i - 1][1], profit[i - (m + 1)][0] - prices[i]);
}
return profit[n - 1][0];
}
int[][][] profit = new int[n][k + 1][2];
for (int i = 0; i < n; i++) {
for (int j = 0; j < k + 1; j++) {
// 处理初始状态
if (i == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
if (j == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
// 处理有交易冷却期时,前 m + 1 天的情况,由于这时候不能卖出,所以前一天卖出状态的最大收益都为0
if (i < m + 1) {
profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i] - fee);
profit[i][j][1] = Math.max(profit[i - 1][j][1], 0 - prices[i]);
continue;
}
// 核心,状态转移方程
profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i] - fee);
profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - (m + 1)][j - 1][0] - prices[i]);
}
}
return profit[n - 1][k][0];
}
从上面函数可以看出,i
表示的天数这一维度可以省略,但如果有交易冷却期这个条件的话,需要额外添加一个数组来保存 i - (m + 1)
天前的值,优化后的代码如下
public int maxProfit(int k, int[] prices, int fee, int m) {
if (k == 0 || prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
if (k > (n >> 1)) {
int sell = 0;
int buy = Integer.MIN_VALUE + fee;
// 保存 i - (m + 1) 天前的值
int[] preSells = new int[m + 1];
for (int i = 0; i < prices.length; i++) {
sell = Math.max(sell, buy + prices[i] - fee);
buy = Math.max(buy, preSells[i % (m + 1)] - prices[i]);
preSells[i % (m + 1)] = sell;
}
return sell;
}
int[] sells = new int[k];
int[] buys = new int[k];
// 保存 i - (m + 1) 天前的值
int[][] preSells = new int[k][m + 1];
// 处理初始状态
for (int i = 0; i < k; i++) {
sells[i] = 0;
buys[i]= Integer.MIN_VALUE + fee;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
if (j == 0) {
sells[j] = Math.max(sells[j], buys[j] + prices[i] - fee);
buys[j] = Math.max(buys[j], -prices[i]);
preSells[j][i % (m + 1)] = sells[j];
continue;
}
sells[j] = Math.max(sells[j], buys[j] + prices[i] - fee);
buys[j] = Math.max(buys[j], preSells[j - 1][i % (m + 1)] - prices[i]);
preSells[j][i % (m + 1)] = sells[j];
}
}
return sells[k - 1];
}
这个系列所有问题都可以在上面的代码基础上进行修改优化,去除不必要的代码即可得出解
应用
利用以上的通用解,解决 Leetcode
的具体题目,从最困难的题目开始到最简单的题目,由深到浅地理解通用解的每一行代码。
只能交易 k 次
Leetcode
的 188 题
由于只有一个交易次数的条件,所以不需要 m
,也不需要 fee
,直接简化代码即可
public int maxProfit(int k, int[] prices) {
if (k == 0 || prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
// 进行一次完全的交易需要两天,所以当 k > n/2 的时候,就可以每天都进行一次买入(卖出)操作,也就是可以交易无数次
if (k > (n >> 1)) {
int[][] profit = new int[n][2];
for (int i = 0; i < n; i++) {
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
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];
}
int[][][] profit = new int[n][k + 1][2];
for (int i = 0; i < n; i++) {
for (int j = 0; j < k + 1; j++) {
if (i == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
if (j == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
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];
}
优化
public int maxProfit(int k, int[] prices) {
if (k == 0 || prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
if (k > (n >> 1)) {
int sell = 0;
int buy = Integer.MIN_VALUE;
for (int price : prices) {
sell = Math.max(sell, buy + price);
buy = Math.max(buy, sell - price);
}
return sell;
}
int[] sells = new int[k];
int[] buys = new int[k];
for (int i = 0; i < k; i++) {
sells[i] = 0;
buys[i]= Integer.MIN_VALUE;
}
for (int price : prices) {
for (int i = 0; i < k; i++) {
if (i == 0) {
sells[i] = Math.max(sells[i], buys[i] + price);
buys[i] = Math.max(buys[i], -price);
continue;
}
sells[i] = Math.max(sells[i], buys[i] + price);
buys[i] = Math.max(buys[i], sells[i - 1] - price);
}
}
return sells[k - 1];
}
只能交易两次
Leetcode
的 123 题
由于只有一个交易次数的条件,所以不需要 m
,也不需要 fee
,直接简化代码得
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int k = 2;
int n = prices.length;
int[][][] profit = new int[n][k + 1][2];
for (int i = 0; i < n; i++) {
for (int j = 0; j < k + 1; j++) {
if (i == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
if (j == 0) {
profit[i][j][0] = 0;
profit[i][j][1] = -prices[0];
continue;
}
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];
}
优化,由于只能交易两次,所以只需要两组变量来保存结果即可
public int maxProfit(int[] prices) {
int sell1 = 0;
int buy1 = Integer.MIN_VALUE;
int sell2 = 0;
int buy2 = Integer.MIN_VALUE;
for (int price : prices) {
sell1 = Math.max(sell1, buy1 + price);
buy1 = Math.max(buy1, -price);
sell2 = Math.max(sell2, buy2 + price);
buy2 = Math.max(buy2, sell1 - price);
}
return sell2;
}
只能交易一次
Leetcode
的 121 题
由于只有一个交易次数的条件,所以不需要 m
,也不需要 fee
;同时因为只能交易一次,所以 k = 1
,也就是可以省略
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
int[][] profit = new int[n][2];
for (int i = 0; i < n; i++) {
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
// 因为只能交易一次,所以这里的 profit[i - 1][0] 恒等于 0
profit[i][1] = Math.max(profit[i - 1][1], -prices[i]);
}
return profit[n - 1][0];
}
优化
public int maxProfit(int[] prices) {
int sell = 0;
int buy = Integer.MIN_VALUE;
for (int price : prices) {
sell = Math.max(sell, buy + price);
buy = Math.max(buy, -price);
}
return sell;
}
这个题目有更通俗直接的解法
// 遍历的同时,保存数组中的最小值,更新最大差值
public int maxProfit(int[] prices) {
int profit = 0;
int min = Integer.MAX_VALUE;
for (int price : prices) {
if (price < min) {
min = price;
} else if (price - min > profit) {
profit = price - min;
}
}
return profit;
}
可以交易无数次
Leetcode
的 122 题
由于只一个交易次数的条件,所以不需要 m
,也不需要 fee
;至于 k
这一维度,也可以删除,有两种理解方式
- 可以交易无数次,也就是
k = +∞
,这时候k ≈ k - 1
,所以k
可以认为是没有作用的,可以删除k
这一维度 - 之前需要
k
这一维度是因为有交易次数限制,每天都要进行遍历,从k
次交易内选择最优方案;但如果没有交易次数限制,则可以认为每天都进行交易,收益可以一直累加,下一天直接取之前的最优方案即可,所以可以删除k
这一维度
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
int[][] profit = new int[n][2];
for (int i = 0; i < n; i++) {
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
// 这里的 profit[i - 1][0] 表示前一天的最优方案,因为没有交易次数限制,也就是收益可以一直累加
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
}
return profit[n - 1][0];
}
优化
public int maxProfit(int[] prices) {
int sell = 0;
int buy = Integer.MIN_VALUE;
for (int price : prices) {
sell = Math.max(sell, buy + price);
buy = Math.max(buy, sell - price);
}
return sell;
}
可以看出只能交易一次与可以交易无数次的区别:
- 只能交易一次:前一天的收益恒为 0
- 可以交易无数次:收益可以一直累加
更通俗直接的解法,贪心算法
// 由于不限交易次数,所以只要后面的价格比较大,都能获得收益
public int maxProfit(int[] prices) {
int maxProfit = 0;
for (int i = 0; i < prices.length - 1; i++) {
int today = prices[i];
int tomorrow = prices[i + 1];
if (today < tomorrow) {
maxProfit += tomorrow - today;
}
}
return maxProfit;
}
可以交易无数次,有一天的冷却期
Leetcode
的 309 题
在可以交易无数次的条件上,加上 m
这个条件即可
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
// 如果冷却期是 n 天的话,让 m = n 即可
int m = 1;
int n = prices.length;
int[][] profit = new int[n][2];
for (int i = 0; i < n; i++) {
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
if (i < m + 1) {
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
profit[i][1] = Math.max(profit[i - 1][1], 0 - prices[i]);
continue;
}
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 - (m + 1)][0] - prices[i]);
}
return profit[n - 1][0];
}
优化
public int maxProfit(int[] prices) {
// 如果冷却期是 n 天的话,让 m = n 即可
int m = 1;
int sell = 0;
int buy = Integer.MIN_VALUE;
int[] preSells = new int[m + 1];
for (int i = 0; i < prices.length; i++) {
sell = Math.max(sell, buy + prices[i]);
buy = Math.max(buy, preSells[i % (m + 1)] - prices[i]);
preSells[i % (m + 1)] = sell;
}
return sell;
}
可以交易无数次,无冷却期,有手续费
Leetcode
的 714 题
在可以交易无数次的条件上,加上 fee
这个条件即可
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
int[][] profit = new int [n][2];
for (int i = 0; i < n; i++) {
if (i == 0) {
profit[i][0] = 0;
profit[i][1] = -prices[0];
continue;
}
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
}
return profit[n - 1][0];
}
优化
public int maxProfit(int[] prices) {
int sell = 0;
// 以防溢出
int buy = Integer.MIN_VALUE + fee;
for (int price : prices) {
sell = Math.max(sell, buy + price - fee);
buy = Math.max(buy, sell - price);
}
return sell;
}
总结
从最困难的题目开始入手这个系列解法或许有点令人难以接受,但这反而是我个人认为最能全局深刻理解掌握这个系列的办法,最复杂的情况都解决了,剩下的就会变得越来越简单。我推荐先从头到尾认真阅读一次,有个全局的印象,再从尾到头阅读一次,理解当条件越来越复杂的时候,代码是怎么改变的,怎么兼容的。相信各位读者看完以上这么多不同条件下的解法以及优化后的代码,一定会对 动态规划
有更深的理解,如果有更优的解法,欢迎一起探讨。