LeetCode 全站第一,牛逼!

本文地址:https://leetcode-cn.com/circle/article/qiAgHn/web

原文出處:https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems算法

你們好,我是吳師兄,今天給你們分享一篇關於股票問題系列的文章,掌握後輕輕鬆鬆秒殺 LeetCode 上全部的股票問題。
數組

本文的做者爲 Storm,目前在力扣上全站排名第一,已獲做者受權轉載此文,但願對你刷題有幫助。微信

前言

此文爲轉載翻譯,和原文相比,這篇文章多了未優化空間的代碼,且代碼都從新寫了,另外更改了部分文字描述。編輯器

股票問題一共有六道題:flex

  • 12一、買賣股票的最佳時機
  • 12二、買賣股票的最佳時機 II
  • 12三、買賣股票的最佳時機 III
  • 18八、買賣股票的最佳時機 IV
  • 30九、最佳買賣股票時機含冷凍期
  • 71四、買賣股票的最佳時機含手續費

每一個問題都有優質的題解,可是大多數題解沒有創建起這些問題之間的聯繫,也沒有給出股票問題系列的通解。優化

這篇文章給出適用於所有股票問題的通解,以及對於每一個特定問題的特解。url


1、通用狀況

這個想法基於以下問題:給定一個表示天天股票價格的數組,什麼因素決定了能夠得到的最大收益?spa

相信大多數人能夠很快給出答案,例如「在哪些天進行交易以及容許多少次交易」。.net

這些因素固然重要,在問題描述中也有這些因素。

然而還有一個隱藏可是關鍵的因素決定了最大收益,下文將闡述這一點。

首先介紹一些符號:

  • n 表示股票價格數組的長度;
  • i 表示第 i 天( i 的取值範圍是 0n - 1);
  • k 表示容許的最大交易次數;
  • T[i][k] 表示在第 i 天結束時,最多進行 k 次交易的狀況下能夠得到的最大收益。

基準狀況是顯而易見的:T[-1][k] = T[i][0] = 0,表示沒有進行股票交易時沒有收益(注意第一天對應 i = 0,所以 i = -1 表示沒有股票交易)。

如今若是能夠將 T[i][k] 關聯到子問題,例如 T[i - 1][k]T[i][k - 1]T[i - 1][k - 1] 等子問題,就能獲得狀態轉移方程,並對這個問題求解。

如何獲得狀態轉移方程呢?

最直接的辦法是看第 i 天可能的操做。有多少個選項?答案是三個:買入賣出休息

應該選擇哪一個操做?

答案是:並不知道哪一個操做是最好的,可是能夠經過計算獲得選擇每一個操做能夠獲得的最大收益。

假設沒有別的限制條件,則能夠嘗試每一種操做,並選擇能夠最大化收益的一種操做。

可是,題目中確實有限制條件,規定不能同時進行屢次交易,所以若是決定在第 i 天買入,在買入以前必須持有 0 份股票,若是決定在第 i 天賣出,在賣出以前必須剛好持有 1 份股票。

持有股票的數量是上文說起到的隱藏因素,該因素影響第 i 天能夠進行的操做,進而影響最大收益。

所以對 T[i][k]的定義須要分紅兩項:

  • T[i][k][0] 表示在第 i 天結束時,最多進行 k 次交易且在進行操做後持有 0 份股票的狀況下能夠得到的最大收益;
  • T[i][k][1] 表示在第 i 天結束時,最多進行 k 次交易且在進行操做後持有 1 份股票的狀況下能夠得到的最大收益。使用新的狀態表示以後,能夠獲得基準狀況和狀態轉移方程。

基準狀況:

T[-1][k][0] = 0, T[-1][k][1] = -Infinity
T[i][0][0] = 0, T[i][0][1] = -Infinity

狀態轉移方程:

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])

基準狀況中,T[-1][k][0] = T[i][0][0] = 0 的含義和上文相同,T[-1][k][1] = T[i][0][1] = -Infinity 的含義是在沒有進行股票交易時不容許持有股票。

對於狀態轉移方程中的 T[i][k][0],第 i 天進行的操做只能是休息或賣出,由於在第 i 天結束時持有的股票數量是 0。T[i - 1][k][0] 是休息操做能夠獲得的最大收益,T[i - 1][k][1] + prices[i] 是賣出操做能夠獲得的最大收益。

注意到容許的最大交易次數是不變的,由於每次交易包含兩次成對的操做,買入和賣出。

只有買入操做會改變容許的最大交易次數。

對於狀態轉移方程中的 T[i][k][1],第 i 天進行的操做只能是休息或買入,由於在第 i 天結束時持有的股票數量是 1。T[i - 1][k][1] 是休息操做能夠獲得的最大收益,T[i - 1][k - 1][0] - prices[i] 是買入操做能夠獲得的最大收益。

注意到容許的最大交易次數減小了一次,由於每次買入操做會使用一次交易。

爲了獲得最後一天結束時的最大收益,能夠遍歷股票價格數組,根據狀態轉移方程計算 T[i][k][0]T[i][k][1] 的值。最終答案是 T[n - 1][k][0],由於結束時持有 0 份股票的收益必定大於持有 1 份股票的收益。

2、應用於特殊狀況

上述六個股票問題是根據 k 的值進行分類的,其中 k 是容許的最大交易次數。最後兩個問題有附加限制,包括「冷凍期」和「手續費」。通解能夠應用於每一個股票問題。

狀況一:k = 1

狀況一對應的題目是「121. 買賣股票的最佳時機」。

對於狀況一,天天有兩個未知變量: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];
        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;
    }
}

如今對上述解法進行分析。對於循環中的部分,profit1 實際上只是表示到第 i 天的股票價格的相反數中的最大值,或者等價地表示到第 i 天的股票價格的最小值。對於 profit0,只須要決定賣出和休息中的哪項操做能夠獲得更高的收益。若是進行賣出操做,則買入股票的價格爲 profit1,即第 i 天以前(不含第 i 天)的最低股票價格。

這正是現實中爲了得到最大收益會作的事情。可是這種作法不是惟一適用於這種狀況的解決方案。

狀況二:k 爲正無窮

狀況二對應的題目是「122. 買賣股票的最佳時機 II」。

若是 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;
    }
}

這個解法提供了得到最大收益的貪心策略:可能的狀況下,在每一個局部最小值買入股票,而後在以後遇到的第一個局部最大值賣出股票。這個作法等價於找到股票價格數組中的遞增子數組,對於每一個遞增子數組,在開始位置買入並在結束位置賣出。

能夠看到,這和累計收益是相同的,只要這樣的操做的收益爲正。

狀況三:k = 2

狀況三對應的題目是「123. 買賣股票的最佳時機 III」。

狀況三和狀況一類似,區別之處是,對於狀況三,天天有四個未知變量: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;
    }
}

狀況四:k 爲任意值

狀況四對應的題目是「188. 買賣股票的最佳時機 IV」。

狀況四是最通用的狀況,對於每一天須要使用不一樣的 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 的值很大的時候會超出時間限制。

狀況五:k 爲正無窮但有冷卻時間

狀況五對應的題目是「309. 最佳買賣股票時機含冷凍期」。

因爲具備相同的 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;
    }
}

狀況六:k 爲正無窮但有手續費

狀況六對應的題目是「714. 買賣股票的最佳時機含手續費」。

因爲具備相同的 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) 的解法。

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)。

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;
    }
}

3、總結

總而言之,股票問題最通用的狀況由三個特徵決定:當前的天數 i容許的最大交易次數 k 以及天天結束時持有的股票數

這篇文章闡述了最大利潤的狀態轉移方程和終止條件,由此能夠獲得時間複雜度爲 O(nk) 和空間複雜度爲 O(k) 的解法。

該解法能夠應用於六個問題,對於最後兩個問題,須要將狀態轉移方程進行一些修改。

本文分享自微信公衆號 - 五分鐘學算法(CXYxiaowu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息