淺談什麼是動態規劃以及相關的「股票」演算法題

程式設計師吳師兄發表於2019-05-13

動態規劃

1 概念

動態規劃演算法是通過拆分問題,定義問題狀態和狀態之間的關係,使得問題能夠以遞推(或者說分治)的方式去解決。在學習動態規劃之前需要明確掌握幾個重要概念。

階段:對於一個完整的問題過程,適當的切分為若干個相互聯絡的子問題,每次在求解一個子問題,則對應一個階段,整個問題的求解轉化為按照階段次序去求解。

狀態:狀態表示每個階段開始時所處的客觀條件,即在求解子問題時的已知條件。狀態描述了研究的問題過程中的狀況。

決策:決策表示當求解過程處於某一階段的某一狀態時,可以根據當前條件作出不同的選擇,從而確定下一個階段的狀態,這種選擇稱為決策。

策略:由所有階段的決策組成的決策序列稱為全過程策略,簡稱策略。

最優策略:在所有的策略中,找到代價最小,效能最優的策略,此策略稱為最優策略。

狀態轉移方程:狀態轉移方程是確定兩個相鄰階段狀態的演變過程,描述了狀態之間是如何演變的。

2 使用場景

能採用動態規劃求解的問題的一般要具有 3 個性質:

(1)最優化:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。子問題的區域性最優將導致整個問題的全域性最優。換句話說,就是問題的一個最優解中一定包含子問題的一個最優解。

(2)無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關,與其他階段的狀態無關,特別是與未發生的階段的狀態無關。

(3)重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃演算法同其他演算法相比就不具備優勢)

3 演算法流程

(1)劃分階段:按照問題的時間或者空間特徵將問題劃分為若干個階段。   (2)確定狀態以及狀態變數:將問題的不同階段時期的不同狀態描述出來。   (3)確定決策並寫出狀態轉移方程:根據相鄰兩個階段的各個狀態之間的關係確定決策。   (4)尋找邊界條件:一般而言,狀態轉移方程是遞推式,必須有一個遞推的邊界條件。   (5)設計程式,解決問題

實戰練習

下面的三道演算法題都是來源於 LeetCode 上與股票買賣相關的問題 ,我們按照 動態規劃 的演算法流程來處理該類問題。

股票買賣這一類的問題,都是給一個輸入陣列,裡面的每個元素表示的是每天的股價,並且你只能持有一支股票(也就是你必須在再次購買前出售掉之前的股票),一般來說有下面幾種問法:

  • 只能買賣一次
  • 可以買賣無數次
  • 可以買賣 k 次

需要你設計一個演算法去獲取最大的利潤。

買賣股票的最佳時機

題目來源於 LeetCode 上第 121 號問題:買賣股票的最佳時機。題目難度為 Easy,目前通過率為 49.4% 。

題目描述

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

如果你最多隻允許完成一筆交易(即買入和賣出一支股票),設計一個演算法來計算你所能獲取的最大利潤。

注意你不能在買入股票前賣出股票。

示例 1:

輸入: [7,1,5,3,6,4]
輸出: 5
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。
     注意利潤不能是 7-1 = 6, 因為賣出價格需要大於買入價格。
複製程式碼

示例 2:

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

題目解析

我們按照動態規劃的思想來思考這道問題。

狀態

買入(buy)賣出(sell) 這兩種狀態。

轉移方程

對於買來說,買之後可以賣出(進入賣狀態),也可以不再進行股票交易(保持買狀態)。

對於賣來說,賣出股票後不在進行股票交易(還在賣狀態)。

只有在手上的錢才算錢,手上的錢購買當天的股票後相當於虧損。也就是說當天買的話意味著損失-prices[i],當天賣的話意味著增加prices[i],當天賣出總的收益就是 buy+prices[i]

所以我們只要考慮當天買和之前買哪個收益更高,當天賣和之前賣哪個收益更高。

  • buy = max(buy, -price[i]) (注意:根據定義 buy 是負數)
  • sell = max(sell, prices[i] + buy)

邊界

第一天 buy = -prices[0], sell = 0,最後返回 sell 即可。

程式碼實現

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1)
            return 0;
        int buy = -prices[0], sell = 0;
        for(int i = 1; i < prices.length; i++) {
            buy = Math.max(buy, -prices[i]);
            sell = Math.max(sell, prices[i] + buy);
        
        }
        return sell;
    }
}
複製程式碼

買賣股票的最佳時機 II

題目來源於 LeetCode 上第 122 號問題:買賣股票的最佳時機 II。題目難度為 Easy,目前通過率為 53.0% 。

題目描述

給定一個陣列,它的第 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。
複製程式碼

題目解析

狀態

買入(buy)賣出(sell) 這兩種狀態。

轉移方程

對比上題,這裡可以有無限次的買入和賣出,也就是說 買入 狀態之前可擁有 賣出 狀態,所以買入的轉移方程需要變化。

  • buy = max(buy, sell - price[i])
  • sell = max(sell, buy + prices[i] )

邊界

第一天 buy = -prices[0], sell = 0,最後返回 sell 即可。

程式碼實現

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1)
            return 0;
        int buy = -prices[0], sell = 0;
        for(int i = 1; i < prices.length; i++) {
            sell = Math.max(sell, prices[i] + buy);
            buy = Math.max( buy,sell - prices[i]);
        }
        return sell;
    }
}
複製程式碼

買賣股票的最佳時機 III

題目來源於 LeetCode 上第 123 號問題:買賣股票的最佳時機 III。題目難度為 Hard,目前通過率為 36.1% 。

題目描述

給定一個陣列,它的第 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。

複製程式碼

題目解析

這裡限制了最多兩筆交易。

狀態

第一次買入(fstBuy)第一次賣出(fstSell)第二次買入(secBuy)第二次賣出(secSell) 這四種狀態。

轉移方程

這裡最多兩次買入和兩次賣出,也就是說 買入 狀態之前可擁有 賣出 狀態,賣出 狀態之前可擁有 買入 狀態,所以買入和賣出的轉移方程都需要變化。

  • fstBuy = max(fstBuy , -price[i])
  • fstSell = max(fstSell,fstBuy + prices[i] )
  • secBuy = max(secBuy ,fstSell -price[i]) (受第一次賣出狀態的影響)
  • secSell = max(secSell ,secBuy + prices[i] )

邊界

  • 一開始 fstBuy = -prices[0]

  • 買入後直接賣出,fstSell = 0

  • 買入後再賣出再買入,secBuy - prices[0]

  • 買入後再賣出再買入再賣出,secSell = 0

最後返回 secSell 。

程式碼實現

class Solution {
    public int maxProfit(int[] prices) {
        int fstBuy = Integer.MIN_VALUE, fstSell = 0;
        int secBuy = Integer.MIN_VALUE, secSell = 0;
        for(int i = 0; i < prices.length; i++) {
            fstBuy = Math.max(fstBuy, -prices[i]);
            fstSell = Math.max(fstSell, fstBuy + prices[i]);
            secBuy = Math.max(secBuy, fstSell -  prices[i]);
            secSell = Math.max(secSell, secBuy +  prices[i]); 
        }
        return secSell;
        
    }
}
複製程式碼

END

如果你覺得這篇內容對你挺有啟發,那麼你可以:

1、點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)

2、關注我,讓我們成為長期關係。

3、關注公眾號「五分鐘學演算法」,裡面已有 150 多篇與演算法有關原創文章。

相關文章