letcode貪心算法練習+6個股票問題總結

Date : 2019-08-21

 

1.  寶石與石頭

給定字符串J 代表石頭中寶石的類型,和字符串 S代表你擁有的石頭。 S 中每個字符代表了一種你擁有的石頭的類型,你想知道你擁有的石頭中有多少是寶石。

J 中的字母不重複,J 和 S中的所有字符都是字母。字母區分大小寫,因此"a"和"A"是不同類型的石頭。

示例 1:

輸入: J = "aA", S = "aAAbbbb"
輸出: 3


示例 2:

輸入: J = "z", S = "ZZ"
輸出: 0

思路:只需要找出S中元素在J中出現過的元素的個數。

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        res = [x for x in S if x in J]  # S中在J中出現的元素;同理可以延伸S中不在J中出現的元素
        return len(res)
        

貪心算法原理

貪心算法就是做出一系列選擇來使原問題達到最優解。在每一個決策點,都是做出當前看來的最優選擇,比如在活動選擇問題裏面,我們總是在一個問題的基礎上選擇結束時間最早的活動,之後再在剩下活動的基礎上選出結束時間最早的活動,以此類推,直到沒有活動可以進行選擇。但是遺憾的是這種算法並不是總能得到最優解,並且是否能得到最優解還取決於對於貪心策略的選擇。

一般來說,設計貪心算法涉及到下面幾個步驟


  1.確定問題的最優子結構 
  2.基於問題的最優子結構設計一個遞歸算法 
  3.證明我們做出的貪心選擇,只剩下一個子問題 
  4.證明貪心選擇總是安全的 
  5.設計一個遞歸算法實現貪心策略 
  6.將貪心算法轉化爲迭代算法


比如在活動選擇問題裏面,我們就是確定了互動最優子結構的性質,我們在子問題SjSj裏面選出一個基於上次選擇ajaj的最早結束活動amam,使得SjSj的最優解是由amam和SmSm的最優解組成的。

更一般的來說,我們可以講貪心算法的設計步驟簡述爲下面幾部:


  1.將最優化問題簡化爲這樣的形式:最初一個選擇以後,只剩下一個子問題需要求解! 
  2.證明在做出貪心選擇以後,原問題總是存在最優解,即貪心選擇總是安全的! 
  3.證明在做出貪心選擇以後,剩下的子問題滿足性質:其最優解與做出選擇的組合在一起得到原問題的最優解,即最優子結構


貪心算法的兩大性質

貪心算法有兩個重要的性質:

  
  貪心選擇性質
  最優子結構性質

下面我們來詳細討論這兩個性質

貪心選擇性質

  第一個關鍵要素就是貪心選擇性質:我們可以做出局部最優選擇來構造最優解。也就是說,我們在做出選擇時,總是以當前的情況爲基礎做出最優選擇的,而不用考慮子問題的解!

這要是和動態規劃最大的不同之處,我們知道

   在動態規劃中,在每次做出一個選擇的時候總是要將所有選擇進行比較以後才能確定到底採用哪一種選擇,而這種選擇的參考依據是以子問題的解爲基礎的,所以動態規劃總是採用自下而下的方法來,先得到子問題的解,再通過子問題的解構造原問題的解。就算是自上而下的算法也是先求出子問題的解,通過遞歸調用自下而上的返回每一個子問題的最優解
  在貪心算法中,我們總是在原問題的基礎上做出一個選擇,然後求解剩下的唯一子問題,貪心算法從來都不依賴子問題的解,不過有可能會依賴上一次做出的選擇,所以貪心算法是自上而下的。一步一步的選擇將原問題一步步消減得更小當然,我們必須證明每一個步驟做出的貪心選擇都可以生成全局最優解!我們再活動選擇問題裏面是這樣的做的,首先假定有一個最優解,然後將做出的選擇替換進去得到另外一個最優解!

最優子結構

如果一個問題的最優解包含其子問題的最優解,那麼就稱這個問題具有最優子結構性質!我們知道最優子結構這個性質是動態規劃和貪心算法都必須具備的關鍵性質。

貪心算法vs動態規劃

貪心算法和動態規劃都有一些共同的性質,比如最優子結構,有些問題我們可以採用動態規劃來解決,也可以採用貪心算法來結局,這兩者之間有細微的差別!下面我們通過研究一個問題來區分之間的差別! 
下面有兩個經典的算法問題:

   0-1揹包問題:我們有一堆物品S={a1,a2,...,an}S={a1,a2,...,an},每一個物品aiai都有一個重量wiwi和一個價值vivi.現在有一個揹包,這個揹包的容量爲WW,現在要將這些物品在不超出揹包容量的情況下選擇性的放入揹包,使得揹包裏面物品的價值最大,物品不能只選取其中一部分,必須選擇整個,或者不選!
   分數揹包問題:這個問題和上面的問題比較相似,唯一不同的就是該問題裏面的物品可以進行分割,即可以只選取一個物品aiai的一部分
  
雖然上面兩個問題比較相似,但是貪心算法可以求解第二個問題而不能求解0-1揹包問題,爲了求解分數揹包問題,我們首先得到每一個物品單位重量的價值vi/wivi/wi,那麼我們要設計一個貪心策略來使得裝入揹包物品的價值最大。我們的第一直覺肯定是要選擇單位重量價格最高的嘍,讓後再選擇物品裏面第二高的,一次類推直到裝滿揹包爲止!
 
  1.  刪列造序

給定由 N 個小寫字母字符串組成的數組 A,其中每個字符串長度相等。

刪除 操作的定義是:選出一組要刪掉的列,刪去 A 中對應列中的所有字符,形式上,第 n 列爲 [A[0][n], A[1][n], ..., A[A.length-1][n]])。

比如,有 A = ["abcdef", "uvwxyz"],

 

要刪掉的列爲 {0, 2, 3},刪除後 A 爲["bef", "vyz"], A 的列分別爲["b","v"], ["e","y"], ["f","z"]。

 

你需要選出一組要刪掉的列 D,對 A 執行刪除操作,使 A 中剩餘的每一列都是 非降序 排列的,然後請你返回 D.length 的最小可能值。

 

示例 1:

輸入:["cba", "daf", "ghi"]
輸出:1
解釋:
當選擇 D = {1},刪除後 A 的列爲:["c","d","g"] 和 ["a","f","i"],均爲非降序排列。
若選擇 D = {},那麼 A 的列 ["b","a","h"] 就不是非降序排列了。


示例 2:

輸入:["a", "b"]
輸出:0
解釋:D = {}


示例 3:

輸入:["zyx", "wvu", "tsr"]
輸出:3
解釋:D = {0, 1, 2}


 

提示:


    1 <= A.length <= 100
    1 <= A[i].length <= 1000

思路:本題實際上將列表中每個元素的對應位置的元素排成一個新列表,然後計算不是非減序列的個數。

class Solution:
    def minDeletionSize(self, A: List[str]) -> int:
        idx = 0
        for i in range(len(A[0])):
            for j in range(1, len(A)):
                if A[j-1][i] > A[j][i]:
                    idx += 1
                    break
        return idx

或者:

class Solution:
    def minDeletionSize(self, A: List[str]) -> int:
            return sum([list(c) != sorted(c) for c in zip(*A)])

2.  最後一塊石頭的重量

有一堆石頭,每塊石頭的重量都是正整數。

每一回合,從中選出兩塊最重的石頭,然後將它們一起粉碎。假設石頭的重量分別爲 x 和 y,且 x <= y。那麼粉碎的可能結果如下:


    如果 x == y,那麼兩塊石頭都會被完全粉碎;
    如果 x != y,那麼重量爲 x 的石頭將會完全粉碎,而重量爲 y 的石頭新重量爲 y-x。


最後,最多隻會剩下一塊石頭。返回此石頭的重量。如果沒有石頭剩下,就返回 0。

class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:
        import bisect 
        stones.sort()  # 升序排列石頭列表
        while len(stones)>1:
            x = stones.pop() # 刪除最後一個元素並取值到x
            y = stones.pop() # 繼續刪除最後一個元素並取值到y
            if x != y:
                bisect.insort(stones, x-y)  # 如不相等,則進行差值的相應位置放置
        return stones[0] if len(stones)==1 else 0  # 返回元素

3.  股票的時間 

1) 只允許一次交易的最佳買股票時間: 買賣股票的最佳時機 I  (k=1)

【貪婪算法思想?】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_p,max_p = 9999,0
        for i in range(len(prices)):
            min_p = min(min_p, prices[i])    # 最小值的買入值
            max_p = max(max_p, prices[i]-min_p)  # 比較之前的最大收益和今天賣出的收益
        return max_p

2) 可以進行多次交易的最佳買賣股票時間(獲取最大收益) 買賣股票的最佳時機 II (k=+inifity)

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:

輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
     隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。


示例 2:

輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。
     因爲這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。


示例 3:

輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤爲 0。

思路一

:該題同樣可以採用DP思想,也可以使用貪婪算法思想:一次遍歷股票價格列表,如果今天的價格小於明天的價格,則採取今天買入,明天賣出的手段進行獲利。

【無數次的交易無限制,貪婪算法更好一些,動態規劃不引入k,與貪婪算法思想有點相似】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices) <= 1:
            return 0
        res = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                res += prices[i] - prices[i-1]
        return res
        

DP思想:

換一種思路:只要第二天股票上升了,賣了就是賺了。實際上這種想法獲得的利潤跟上面的一樣。基於這種想法,我們就可以寫出狀態轉移方程:

dp[i] = dp[i-1] if prices[i]<=prices[i-1] else dp[i-1]+(prices[i]-prices[i-1])

也就是說:如果今天比昨天價更高,則dp[i] = dp[i-1]+高出的價格;反之,則不變

另一種動態規劃思想

DP動態規劃,第i天只有兩種狀態,不持有或持有股票,當天不持有股票的狀態可能來自昨天賣出或者昨天也不持有,同理,當天持有股票的狀態可能來自昨天買入或者昨天也持有中,取最後一天的不持有股票狀態就是問題的解。【文末尾有整體的思路】

lass Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[0]*2 for _ in range(n)]
        # dp[i][0]表示第i天不持有股票, dp[i][1]表示第i天持有股票
        dp[0][0], dp[0][1] = 0, - prices[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        return dp[n-1][0]

3) 買賣股票的最佳時機 III  (k=2)

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。

注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:

輸入: [3,3,5,0,0,3,1,4]
輸出: 6
解釋: 在第 4 天(股票價格 = 0)的時候買入,在第 6 天(股票價格 = 3)的時候賣出,這筆交易所能獲得利潤 = 3-0 = 3 。
     隨後,在第 7 天(股票價格 = 1)的時候買入,在第 8 天 (股票價格 = 4)的時候賣出,這筆交易所能獲得利潤 = 4-1 = 3 。

示例 2:

輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。   
     因爲這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。


示例 3:

輸入: [7,6,4,3,1] 
輸出: 0 
解釋: 在這個情況下, 沒有交易完成, 所以最大利潤爲 0。

【3維DP,動態規劃思想】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[[0]*2 for _ in range(3)] for _ in range(n)]
        # dp[i][j][0]表示第i天交易了j次時不持有股票, dp[i][j][1]表示第i天交易了j次時持有股票
        # 定義購買股票時交易次數加1
        for i in range(3):
            dp[0][i][0], dp[0][i][1] = 0, -prices[0]
        
        for i in range(1, n):
            for j in range(3):
                if not j:
                    dp[i][j][0] = dp[i-1][j][0]
                else:
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
        
        return max(dp[n-1][0][0], dp[n-1][1][0], dp[n-1][2][0])

4) 買賣股票的最佳時機 IV  (k=K),不固定的k值

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多可以完成 k 筆交易。

注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:

輸入: [2,4,1], k = 2
輸出: 2
解釋: 在第 1 天 (股票價格 = 2) 的時候買入,在第 2 天 (股票價格 = 4) 的時候賣出,這筆交易所能獲得利潤 = 4-2 = 2 。


示例 2:

輸入: [3,2,6,5,0,3], k = 2
輸出: 7
解釋: 在第 2 天 (股票價格 = 2) 的時候買入,在第 3 天 (股票價格 = 6) 的時候賣出, 這筆交易所能獲得利潤 = 6-2 = 4 。
隨後,在第 5 天 (股票價格 = 0) 的時候買入,在第 6 天 (股票價格 = 3) 的時候賣出, 這筆交易所能獲得利潤 = 3-0 = 3 。

【3維DP,動態規劃思想】

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices or not k:
            return 0
        n = len(prices)
        
        # 對於過大的K,用貪心法解決
        if k > n//2:
            return self.greedy(prices)
        
        dp,res = [[[0]*2 for _ in range(k+1)] for _ in range(n)],[]
        # dp[i][j][0]表示第i天交易了j次時不持有股票, dp[i][j][1]表示第i天交易了j次時持有股票
        # 定義購買股票時交易次數加1
        for i in range(k+1):
            dp[0][i][0], dp[0][i][1] = 0, -prices[0]
        
        for i in range(1, n):
            for j in range(k+1):
                if not j:
                    dp[i][j][0] = dp[i-1][j][0]
                else:
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                    dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
                
        # 所有交易次數最後一天不持有股票的集合的最大值即爲問題的解
        for m in range(k+1):
            res.append(dp[n-1][m][0])
        return max(res)

    # 處理K過大導致超時的問題,用貪心法解決:
    def greedy(self,prices):
        res = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                res += (prices[i] - prices[i-1])
        return res

5) 買賣股票的最佳時機(含手續費)

給定一個整數數組 prices,其中第 i 個元素代表了第 i 天的股票價格 ;非負整數 fee 代表了交易股票的手續費用。

你可以無限次地完成交易,但是你每次交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。

返回獲得利潤的最大值。

示例 1:

輸入: 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.

注意:


    0 < prices.length <= 50000.
    0 < prices[i] < 50000.
    0 <= fee < 50000.

【貪心算法求解】

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n =  len(prices)
        if n <= 1:
            return 0
        res = 0
        minval = prices[0]  # 記錄最小值
        for i in range(1,n):
            if prices[i] < minval :
                minval = prices[i]  # 更新方式1
            elif prices[i] > (minval+fee):
                res += (prices[i]-minval-fee)  # 滿足結果累計
                minval = prices[i]-fee  # 更新方式2
        return res
        

【動態規劃思想求解】

思想解釋:

dp1[i]表示第i天手上有股票,dp2[i]表示第i天手上沒有股票,遞歸方程:

  1. dp1[i] = max(dp1[i-1], dp2[i-1] - prices[i]) (第二項表示在第i天買入股票)
  2. dp2[i] = max(dp2[i-1], dp1[i-1] + prices[i] - fee) (第二項表示在第i天將股票賣出,需扣除手續費)
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        if n <2:
            return 0
        dp1 = [0 for _ in range(n)] # dp1[i]表示第i天手上有股票時的最大收益
        dp2 = [0 for _ in range(n)] # dp2[i]表示第i天手上沒有股票時的最大收益
        dp1[0] = -prices[0]
        for i in range(1,n):
            dp1[i] = max(dp1[i-1], dp2[i-1]-prices[i])
            dp2[i] = max(dp2[i-1], dp1[i-1]+prices[i]-fee) # 規定在賣股票時,要支付相應的手續費
        return dp2[n-1]        

6) 最佳買賣股票時機含冷凍期 

給定一個整數數組,其中第 i 個元素代表了第 i 天的股票價格 。​

設計一個算法計算出最大利潤。在滿足以下約束條件下,你可以儘可能地完成更多的交易(多次買賣一支股票):


    你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
    賣出股票後,你無法在第二天買入股票 (即冷凍期爲 1 天)。


示例:

輸入: [1,2,3,0,2]
輸出: 3 
解釋: 對應的交易狀態爲: [買入, 賣出, 冷凍期, 買入, 賣出]

 

【動態規劃思想】

sell[i]表示截至第i天,最後一個操作是賣時的最大收益;
buy[i]表示截至第i天,最後一個操作是買時的最大收益;
cool[i]表示截至第i天,最後一個操作是冷凍期時的最大收益;
遞推公式:
sell[i] = max(buy[i-1]+prices[i], sell[i-1]) (第一項表示第i天賣出,前一天i-1買入,第二項表示第i天冷凍)
buy[i] = max(cool[i-1]-prices[i], buy[i-1]) (第一項表示第i天買進,前一天i-1冷凍期,第二項表示第i天冷凍)
cool[i] = max(sell[i-1], buy[i-1], cool[i-1])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if n == 0:
            return 0
        sell = [0 for _ in range(n)]
        buy = [0 for _ in range(n)]
        cool = [0 for _ in range(n)]
        buy[0] = -prices[0]
        for i in range(1,n):
            sell[i] = max(buy[i-1]+prices[i],sell[i-1])
            buy[i] = max(cool[i-1]-prices[i],buy[i-1])
            cool[i] = max(sell[i-1],buy[i-1],cool[i-1])
        return sell[-1]

4. 反轉字符串裏的單詞 【小紅書筆試第一題】

給定一個字符串,逐個翻轉字符串中的每個單詞。

 

示例 1:

輸入: "the sky is blue"
輸出: "blue is sky the"


示例 2:

輸入: "  hello world!  "
輸出: "world! hello"
解釋: 輸入字符串可以在前面或者後面包含多餘的空格,但是反轉後的字符不能包括。


示例 3:

輸入: "a good   example"
輸出: "example good a"
解釋: 如果兩個單詞間有多餘的空格,將反轉後單詞間的空格減少到只含一個。

class Solution:
    def reverseWords(self, s: str) -> str:
        return ' '.join(s.split()[::-1])

 

###  利用動態規劃思想整體分析上面的6種股票問題:

第一題是隻進行一次交易,相當於 k = 1;第二題是不限交易次數,相當於 k = +infinity(正無窮);第三題是隻進行 2 次交易,相當於 k = 2;第四題是進行 K次交易,剩下兩道也是不限次數,但是加了交易「冷凍期」和「手續費」的額外條件,

一、窮舉框架

這裏,我們不用遞歸思想進行窮舉,而是利用「狀態」進行窮舉。我們具體到每一天,看看總共有幾種可能的「狀態」,再找出每個「狀態」對應的「選擇」。我們要窮舉所有「狀態」,窮舉的目的是根據對應的「選擇」更新狀態。

選擇: 比如說這個問題,每天都有三種「選擇」:買入、賣出、無操作,我們用 buy, sell, rest 表示這三種選擇。但問題是,並不是每天都可以任意選擇這三種選擇的,因爲 sell 必須在 buy 之後,buy 必須在 sell 之後。那麼 rest 操作還應該分兩種狀態,一種是 buy 之後的 rest(持有了股票),一種是 sell 之後的 rest(沒有持有股票)。而且別忘了,我們還有交易次數 k 的限制,就是說你 buy 還只能在 k > 0 的前提下操作。

狀態:

[狀態] 有三個,第一個是天數,第二個是允許交易的最大次數,第三個是當前的持有狀態(即之前說的 rest 的狀態,我們不妨用 1 表示持有,0 表示沒有持有)。然後我們用一個三維數組就可以裝下這幾種狀態的全部組合:

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 次交易,最多獲得多少利潤?

二、狀態轉移框架

現在,我們完成了「狀態」的窮舉,我們開始思考每種「狀態」有哪些「選擇」,應該如何更新「狀態」。只看「持有狀態」,可以畫個狀態轉移圖。

                              

通過這個圖可以很清楚地看到,每種狀態(0 和 1)是如何轉移而來的。根據這個圖,我們來寫一下狀態轉移方程:

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和狀態轉移方程:

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 = 1

直接套狀態轉移方程,根據 base case,可以做一些化簡:

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
            = max(dp[i-1][1][1], -prices[i])
解釋:k = 0 的 base case,所以 dp[i-1][0][0] = 0。

現在發現 k 都是 1,不會改變,即 k 對狀態轉移已經沒有影響了。
可以進行進一步化簡去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])

第二題,k = +infinity

如果 k 爲正無窮,那麼就可以認爲 k 和 k - 1 是一樣的。可以這樣改寫框架:

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])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我們發現數組中的 k 已經不會改變了,也就是說不需要記錄 k 這個狀態了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

第三題,k = 2

原始的動態轉移方程,沒有可化簡的地方
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 取值範圍比較小,所以可以不用 for 循環,直接把 k = 1 和 2 的情況手動列舉出來也可以:

dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])

  第四題,k = any integer
有了上一題 k = 2 的鋪墊,這題應該和上一題的第一個解法沒啥區別。但是出現了一個超內存的錯誤,原來是傳入的 k 值會非常大,dp 數組太大了。現在想想,交易次數 k 最多有多大呢?
一次交易由買入和賣出構成,至少需要兩天。所以說有效的限制 k 應該不超過 n/2【超過用貪婪算法】,如果超過,就沒有約束作用了,相當於 k = +infinity。這種情況是之前解決過的。

第五題,k = +infinity with cooldown

每次 sell 之後要等一天才能繼續交易。只要把這個特點融入上一題的狀態轉移方程即可:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解釋:第 i 天選擇 buy 的時候,要從 i-2 的狀態轉移,而不是 i-1 。

第六題,k = +infinity with fee

每次交易要支付手續費,只要把手續費從利潤中減去即可。改寫方程:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解釋:相當於買入股票的價格升高了。
在第一個式子裏減也是一樣的,相當於賣出股票的價格減小了。

 
參考資料:

作者:labuladong
鏈接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-w-5/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章