LeetCode題解:股票買賣問題
技術標籤:LeetCode刷題記錄演算法java資料結構pythonleetcode
LeetCode題解:股票問題
自大學開始,我便陸陸續續的學習一些 演算法和資料結構 方面的內容,同時也開始在一些平臺刷題,也會參加一些大大小小的演算法競賽。但是平時刷題缺少目的性、系統性,最終導致演算法方面進步緩慢。最終,為了自己的未來,我決定開始在LeetCode上進行系統的學習和練習,同時將刷題的軌跡整理記錄,分享出來與大家共勉。
參考教材: labuladong的演算法小抄官方完整版
參考資料: 股票問題系列通解
題目列表: https://leetcode-cn.com/list/x87xoxmg
目錄標題
- LeetCode題解:股票問題
- 000.演算法框架
- [121. 買賣股票的最佳時機](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)
- [122. 買賣股票的最佳時機 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)
- [123. 買賣股票的最佳時機 III](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)
- [188. 買賣股票的最佳時機 IV](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/)
- [309. 最佳買賣股票時機含冷凍期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)
- [714. 買賣股票的最佳時機含手續費](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
000.演算法框架
注意: 文章內容基於參考資料進行整理,整理的比較簡單,這裡還是建議 檢視原文 進行學習。
用一個三維陣列就可以裝下這幾種狀態的全部組合:
dp[i][k][0 or 1] 0 <= i <= n-1, 1 <= k <= K n 為天數,⼤ K 為最多交易數 此問題共 n × K × 2 種狀態,全部窮舉就能搞定。 for 0 <= i < n: for 1 <= k <= K: for s in {0, 1}: dp[i][k][s] = max(buy, sell, rest)
而且我們可以⽤⾃然語⾔描述出每⼀個狀態的含義,⽐如說 dp[3][2][1] 的含義就是:今天是第三天,我現在⼿上持有著股票,⾄今最多進行 2 次交 易。再⽐如 dp[2][3][0] 的含義:今天是第⼆天,我現在⼿上沒有持有股 票,⾄今最多進⾏ 3 次交易。
想求的最終答案是 dp[n - 1][K][0], 即最後⼀天, 最多允許 K 次交易,最多獲得多少利潤。
狀態轉移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 選擇 rest , 選擇 sell )
解釋:
今天我沒有持有股票, 有兩種可能:
要麼是我昨天就沒有持有, 然後今天選擇 rest, 所以我今天還是沒有持有;
要麼是我昨天持有股票, 但是今天我 sell 了, 所以我今天沒有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 選擇 rest , 選擇 buy )
解釋:
今天我持有著股票, 有兩種可能:
要麼我昨天就持有著股票, 然後今天選擇 rest, 所以我今天還持有著股票;
要麼我昨天本沒有持有, 但今天我選擇 buy, 所以今天我就持有股票了。
定義 base case , 即最簡單的情況。
dp[-1][k][0] = 0
解釋: 因為 i 是從 0 開始的, 所以 i = -1 意味著還沒有開始, 這時候的利潤當然是 0。
dp[-1][k][1] = -infinity
解釋: 還沒開始的時候, 是不可能持有股票的, ⽤負⽆窮表⽰這種不可能。
dp[i][0][0] = 0
解釋: 因為 k 是從 1 開始的, 所以 k = 0 意味著根本不允許交易, 這時候利潤當然是 0。
dp[i][0][1] = -infinity
解釋: 不允許交易的情況下, 是不可能持有股票的, ⽤負⽆窮表⽰這種不可能。
把上⾯的狀態轉移方程總結⼀下:
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 是允許的最大交易次數。最後兩個問題有附加限制,包括「冷凍期」和「手續費」。通解可以應用於每個股票問題。
121. 買賣股票的最佳時機
難度: 簡單
給定一個數組 prices
,它的第 i
個元素 prices[i]
表示一支給定股票第 i
天的價格。
你只能選擇 某一天 買入這隻股票,並選擇在 未來的某一個不同的日子 賣出該股票。設計一個演算法來計算你所能獲取的最大利潤。
返回你可以從這筆交易中獲取的最大利潤。如果你不能獲取任何利潤,返回 0
。
示例:
輸入:[7,1,5,3,6,4]
輸出:5
解釋:在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。
注意利潤不能是 7-1 = 6, 因為賣出價格需要大於買入價格;同時,你不能在買入前賣出股票。
題解:
情況一:k = 1
對於情況一,每天有兩個未知變數:T[i][1][0] 和 T[i][1][1],狀態轉移方程如下:
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
第二個狀態轉移方程利用了 T[i][0][0] = 0。
根據上述狀態轉移方程,可以寫出時間複雜度為 O(n) 和空間複雜度為 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
//i=0時,dp[i-1]不合法,預處理
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; 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], -prices[i]);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相關,空間複雜度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
profit0 = Math.max(profit0, profit1 + prices[i]);
profit1 = Math.max(profit1, -prices[i]);
}
return profit0;
}
}
122. 買賣股票的最佳時機 II
難度: 簡單
給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。
注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例:
輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。
題解:
情況二:k 為正無窮
如果 k 為正無窮,則 k 和 k - 1 可以看成是相同的,因此有 T[i - 1][k - 1][0] = T[i - 1][k][0] 和 T[i - 1][k - 1][1] = T[i - 1][k][1]。每天仍有兩個未知變數:T[i][k][0] 和 T[i][k][1],其中 k 為正無窮,狀態轉移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i]) = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
第二個狀態轉移方程利用了 T[i - 1][k - 1][0] = T[i - 1][k][0]。
根據上述狀態轉移方程,可以寫出時間複雜度為 O(n) 和空間複雜度為 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; 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[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相關,空間複雜度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i]);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
這個解法提供了獲得最大收益的貪心策略:可能的情況下,在每個區域性最小值買入股票,然後在之後遇到的第一個區域性最大值賣出股票。這個做法等價於找到股票價格陣列中的遞增子陣列,對於每個遞增子陣列,在開始位置買入並在結束位置賣出。可以看到,這和累計收益是相同的,只要這樣的操作的收益為正。
123. 買賣股票的最佳時機 III
難度:困難
給定一個數組,它的第 i
個元素是一支給定的股票在第 i
天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。
注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例:
輸入:prices = [3,3,5,0,0,3,1,4]
輸出:6
解釋:在第 4 天(股票價格 = 0)的時候買入,在第 6 天(股票價格 = 3)的時候賣出,這筆交易所能獲得利潤 = 3-0 = 3 。
隨後,在第 7 天(股票價格 = 1)的時候買入,在第 8 天 (股票價格 = 4)的時候賣出,這筆交易所能獲得利潤 = 4-1 = 3 。
情況三:k = 2
情況三和情況一相似,區別之處是,對於情況三,每天有四個未知變數:T[i][1][0]、T[i][1][1]、T[i][2][0]、T[i][2][1],
狀態轉移方程如下:
T[i][2][0] = max(T[i - 1][2][0], T[i - 1][2][1] + prices[i])
T[i][2][1] = max(T[i - 1][2][1], T[i - 1][1][0] - prices[i])
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
第四個狀態轉移方程利用了 T[i][0][0] = 0。
根據上述狀態轉移方程,可以寫出時間複雜度為 O(n) 和空間複雜度為 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][][] dp = new int[length][3][2];
dp[0][1][0] = 0;
dp[0][1][1] = -prices[0];
dp[0][2][0] = 0;
dp[0][2][1] = -prices[0];
for (int i = 1; i < length; i++) {
dp[i][2][0] = Math.max(dp[i - 1][2][0], dp[i - 1][2][1] + prices[i]);
dp[i][2][1] = Math.max(dp[i - 1][2][1], dp[i - 1][1][0] - prices[i]);
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i]);
dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i]);
}
return dp[length - 1][2][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相關,空間複雜度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profitOne0 = 0, profitOne1 = -prices[0], profitTwo0 = 0, profitTwo1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
profitTwo0 = Math.max(profitTwo0, profitTwo1 + prices[i]);
profitTwo1 = Math.max(profitTwo1, profitOne0 - prices[i]);
profitOne0 = Math.max(profitOne0, profitOne1 + prices[i]);
profitOne1 = Math.max(profitOne1, -prices[i]);
}
return profitTwo0;
}
}
188. 買賣股票的最佳時機 IV
難度: 困難
給定一個整數陣列 prices
,它的第 i
個元素 prices[i]
是一支給定的股票在第 i
天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 k 筆交易。
注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例:
輸入:k = 2, prices = [2,4,1]
輸出:2
解釋:在第 1 天 (股票價格 = 2) 的時候買入,在第 2 天 (股票價格 = 4) 的時候賣出,這筆交易所能獲得利潤 = 4-2 = 2 。
情況四:k 為任意值
情況四是最通用的情況,對於每一天需要使用不同的 k 值更新所有的最大收益,對應持有 0 份股票或 1 份股票。如果 k 超過一個臨界值,最大收益就不再取決於允許的最大交易次數,而是取決於股票價格陣列的長度,因此可以進行優化。那麼這個臨界值是什麼呢?
一個有收益的交易至少需要兩天(在前一天買入,在後一天賣出,前提是買入價格低於賣出價格)。如果股票價格陣列的長度為 n,則有收益的交易的數量最多為 n / 2(整數除法)。因此 k 的臨界值是 n / 2。如果給定的 k 不小於臨界值,即 k >= n / 2,則可以將 k 擴充套件為正無窮,此時問題等價於情況二。
根據狀態轉移方程,可以寫出時間複雜度為 O(nk)和空間複雜度為 O(nk) 的解法。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
if (k >= length / 2) {
return maxProfit(prices);
}
int[][][] dp = new int[length][k + 1][2];
for (int i = 1; i <= k; i++) {
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < length; i++) {
for (int j = k; j > 0; j--) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[length - 1][k][0];
}
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; 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[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相關,空間複雜度可以降到 O(k)。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
if (k >= length / 2) {
return maxProfit(prices);
}
int[][] dp = new int[k + 1][2];
for (int i = 1; i <= k; i++) {
dp[i][0] = 0;
dp[i][1] = -prices[0];
}
for (int i = 1; i < length; i++) {
for (int j = k; j > 0; j--) {
dp[j][0] = Math.max(dp[j][0], dp[j][1] + prices[i]);
dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - prices[i]);
}
}
return dp[k][0];
}
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i]);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
如果不根據 k 的值進行優化,在 k 的值很大的時候會超出時間限制。
309. 最佳買賣股票時機含冷凍期
難度: 中等
給定一個整數陣列,其中第 i 個元素代表了第 i 天的股票價格 。
設計一個演算法計算出最大利潤。在滿足以下約束條件下,你可以儘可能地完成更多的交易(多次買賣一支股票):
- 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
- 賣出股票後,你無法在第二天買入股票 (即冷凍期為 1 天)。
示例:
輸入: [1,2,3,0,2]
輸出: 3
解釋: 對應的交易狀態為: [買入, 賣出, 冷凍期, 買入, 賣出]
情況五:k 為正無窮但有冷卻時間
由於具有相同的 k 值,因此情況五和情況二非常相似,不同之處在於情況五有「冷卻時間」的限制,因此需要對狀態轉移方程進行一些修改。
情況二的狀態轉移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
但是在有「冷卻時間」的情況下,如果在第 i - 1 天賣出了股票,就不能在第 i 天買入股票。因此,如果要在第 i 天買入股票,第二個狀態轉移方程中就不能使用 T[i - 1][k][0],而應該使用 T[i - 2][k][0]。狀態轉移方程中的別的項保持不變,新的狀態轉移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 2][k][0] - prices[i])
根據上述狀態轉移方程,可以寫出時間複雜度為 O(n) 和空間複雜度為 O(n)的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; 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], (i >= 2 ? dp[i - 2][0] : 0) - prices[i]);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天和第 i - 2 天的最大收益相關,空間複雜度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int prevProfit0 = 0, profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int nextProfit0 = Math.max(profit0, profit1 + prices[i]);
int nextProfit1 = Math.max(profit1, prevProfit0 - prices[i]);
prevProfit0 = profit0;
profit0 = nextProfit0;
profit1 = nextProfit1;
}
return profit0;
}
}
714. 買賣股票的最佳時機含手續費
難度: 中等
給定一個整數陣列 prices
,其中第 i
個元素代表了第 i
天的股票價格 ;非負整數 fee
代表了交易股票的手續費用。
你可以無限次地完成交易,但是你每筆交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。
返回獲得利潤的最大值。
注意: 這裡的一筆交易指買入持有並賣出股票的整個過程,每筆交易你只需要為支付一次手續費。
示例:
輸入: prices = [1, 3, 2, 8, 4, 9], fee = 2
輸出: 8
解釋: 能夠達到的最大利潤:
在此處買入 prices[0] = 1
在此處賣出 prices[3] = 8
在此處買入 prices[4] = 4
在此處賣出 prices[5] = 9
總利潤: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
情況六:k 為正無窮但有手續費
由於具有相同的 k 值,因此情況六和情況二非常相似,不同之處在於情況六有「手續費」,因此需要對狀態轉移方程進行一些修改。
情況二的狀態轉移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
由於需要對每次交易付手續費,因此在每次買入或賣出股票之後的收益需要扣除手續費,新的狀態轉移方程有兩種表示方法。
第一種表示方法,在每次買入股票時扣除手續費:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i] - fee)
第二種表示方法,在每次賣出股票時扣除手續費:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i] - fee)
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
根據上述狀態轉移方程,可以寫出時間複雜度為 O(n)O(n) 和空間複雜度為 O(n)O(n) 的解法。
class Solution {
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee;
for (int i = 1; i < length; 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[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相關,空間複雜度可以降到 O(1)O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0] - fee;
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i] - fee);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
作者:耿鬼不會笑
時間:2021年2月