演算法:買賣股票系列
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 天的情況
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 天的情況
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),i - 1]
天前的值,優化後的程式碼如下
public int maxProfit(int k,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),i - 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),i - 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];
}
複製程式碼
這個系列所有問題都可以在上面的程式碼基礎上進行修改優化,去除不必要的程式碼即可得出解
只能交易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][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,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],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;
}
複製程式碼
總結
從最複雜的條件開始思考這個系列解法或許有點反人類,但是我個人覺得這樣才是最能全域性深刻理解掌握這個系列的辦法,最複雜的情況都解決了,剩下的就很簡單了。相信各位讀者看完以上這麼多不同條件下的解法以及優化後的程式碼,一定會對動態規劃
有更深的理解,如果有更優的解法,歡迎一起探討。