1. 程式人生 > 其它 >LeetCode題解:股票買賣問題

LeetCode題解:股票買賣問題

技術標籤:LeetCode刷題記錄演算法java資料結構pythonleetcode

LeetCode題解:股票問題

自大學開始,我便陸陸續續的學習一些 演算法和資料結構 方面的內容,同時也開始在一些平臺刷題,也會參加一些大大小小的演算法競賽。但是平時刷題缺少目的性、系統性,最終導致演算法方面進步緩慢。最終,為了自己的未來,我決定開始在LeetCode上進行系統的學習和練習,同時將刷題的軌跡整理記錄,分享出來與大家共勉。


參考教材: labuladong的演算法小抄官方完整版

參考資料: 股票問題系列通解

題目列表: https://leetcode-cn.com/list/x87xoxmg


目錄標題

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月