大廠常考動態規劃演算法題

公眾號程式設計師學長發表於2021-08-25

大家好,今天我們來聊一聊股票交易問題。

Tips:炒股投資的朋友可以直接走了,這是程式設計師的筆試面試題,不是真正的去探討炒股哦!不過這兩天港股漲的不錯...

​ 前幾天群裡的小夥伴參加位元組面試,遇到了股票交易這麼一道題。今天我們就來分析一下。同時也給即將要參加校招的朋友們提供準備,這是位元組騰訊等大廠校招時常考的題目。

問題描述:

給定一個陣列prices,它的第i個元素 prices[i]表示一支給定股票第i天的價格。你只能選擇某一天買入這隻股票,並選擇在未來的某一個不同的日子賣出該股票。設計一個演算法來計算你所能獲取的最大利潤。返回你可以從這筆交易中獲取的最大利潤。如果你不能獲取任何利潤,返回 0。

分析問題:

​ 拿到這個問題,我們就需要先思考用什麼樣的思想去解決。我們來看這個題目,這個問題是求最優解問題。初看題目,我們最容易想到的方法,就是暴力求解,即遍歷陣列,求出最大值和最小值,相減就是最大利潤。不過這裡有一個隱含條件,就是股票的賣出時機必須大於股票的買入時機。

def maxProfit(prices):
    #最大利潤
    ans = 0
    #股票賣出時機要大於股票買入時機
    for i in range(len(prices)):
        for j in range(i + 1, len(prices)):
            ans = max(ans, prices[j] - prices[i])
    return ans

prices=[7, 1, 5, 3, 6, 4]
maxProfit(prices)

​ 我們可以看出,這種求解的方法,時間複雜度是O(n^2),那有沒有時間複雜度更優的方法呢?

​ 首先,我們來分析一下這個問題符合什麼“問題模型”呢?我們從0開始走,走到n-1,一共需要走n-1步,也就對應著n-1個階段。每個階段都只能往右走,並且每個階段都會對應一個狀態集合,我們把狀態定義為maxprofit[i],其中i表示走到哪一步,maxprofit[i]的值表示從開始走到位置i的最大利潤是多少。所以這個問題是“多階段決策最優”問題,對於這類問題,我們首先要考慮能否用動態規劃的思想來解決,也就是看是否符合動態規劃的特徵:重複子問題、無後效性、最優子結構。

  1. 無後效性:我們要想計算maxprofit[i]這個狀態,只需要關心maxprofit[i-1]的狀態,並不關心maxprofit[i-1]這個狀態是如何生成的。而且,我們只能往後移動,不允許後退。所以,前面階段的狀態確定之後,不會被後面階段的決策所改變。所以符合"無後效性"。

  2. 重複子問題:如果要達到maxprofit[i]這個狀態,我們可以有不同的決策序列。比如假設第五天,我們的最大利潤是8,即maxprofit[i]的狀態為8,我們可以是第一天買入2,第三天賣出10。也可以第二天買入2,第三天賣出10。

  3. 最優子結構:因為maxprofit[i]可以通過maxprofit[i-1]推匯出來,即符合“最優子結構”

    maxprofit[i]=max(maxprofit[i-1],prices[i]-minprice)
    

    下面我們看程式碼如何實現:

def maxProfit(prices):
    n=len(prices)
    if n==0:
        return 0
    maxPRofit=[0]*n
    minprice=prices[0]

    for i in range(1,n):
        minprice = min(minprice, prices[i])
        maxPRofit[i]=max(maxPRofit[i-1],prices[i]-minprice)

    return maxPRofit[-1]

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

​ 我們可以看出,這種求解的時間複雜度為O(n)。

擴充套件:

​ 我們再把問題複雜一點,假如我們可以進行多次交易一支股票。

給定一個陣列 prices,其中 prices[i] 是一支給定股票第 i 天的價格。設計一個演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

​ 這個問題的解題思路和上題類似,也是“多階段決策最優”問題。也符合動態規劃解題模型。不過這裡的“狀態”會有所區別,我們來看一下。

​ 考慮到“不能同時參與多筆交易”。所以,每天交易結束後手裡只可能存在有股票和沒有股票兩種狀態。所以,我們可以用二個陣列來表示,其中dp1[i]表示第i天交易後,手裡沒有股票的最大利潤。dp2[i]表示第i天交易完成後,手裡有股票的最大利潤。

​ dp1[i]表示第i天手裡沒有股票的最大利潤,那麼這個狀態要麼是由dp1[i-1],即前一天手裡沒有股票的最大利潤,轉移過來。要麼由dp2[i-1]+prices[i],即前一天手裡有股票,今天把這個股票賣出。所以dp1[i]的狀態轉移方程為:

dp1[i]=max(dp1[i-1],dp2[i-1]+prices[i])

​ dp2[i]表示第i天手裡有股票的最大利潤,那麼這個狀態要麼是由dp2[i-1],即前一天手裡有股票的最大利潤,轉移過來。要麼由dp1[i-1]-prices[i],即前一天手裡沒有股票,今天把這個股票進行買入。所以dp2[i]的狀態轉移方程為:

dp2[i]=max(dp2[i-1],dp1[i-1]-prices[i])

​ 所以我們程式碼實現如下所示:

def maxProfit(prices):
    n=len(prices)
    if n==0:
        return 0
    dp1=[0]*n
    dp2=[0]*n
    #手裡沒有股票
    dp1[0]=0
    #手裡有股票
    dp2[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])

    return dp1[n-1]

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

​ 我們再來把題目升級一下,即我們限制股票買賣的次數,比如我們只能交易兩次。

給定一個陣列,它的第 i 個元素是一支給定的股票在第 i 天的價格。設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成兩筆交易。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

​ 這個題目和上個題相比,狀態又發生了變化,這裡因為限制了交易次數,所以我們需要把交易次數的狀態也儲存起來。所以每天交易結束後,有5種狀態。

  1. 未進行過任何操作。
  2. 只進行過一次買入操作。
  3. 進行過一次買入和賣出操作。
  4. 完成一筆的交易前提下,進行了第二次買入操作。
  5. 完成兩筆交易。

由於第一個狀態的利潤為0,所以我們不需要記錄。我們把剩下的4個狀態分別用buy1,sell1,buy2,sell2來表示。下面我們來看一下如何根據前一天的狀態,來通過轉移方程生成今天的4個狀態。

​ 對於buy1而言,我們可以今天不操作,直接通過昨天的buy1轉移過來,或者我們從未進行過任何操作前提下,今天買入股票。

​ buy1 =max(buy1,-prices[i])

​ 對於sell1而言,我們今天可以不做任何操作,直接通過昨天的sell1轉移過來,或者我們可以在只進行過一次買入交易的前提下,今天把股票賣出。

​ sell1=max(sell1,buy1+prices[i])

​ 對於buy2而言,我們今天可以不做任何操作,直接通過昨天的buy2轉移過來,或者,我們在進行過一筆完成的交易條件下,今天再把該股票買入。

​ buy2=max(buy2,sell1-prices[i])

​ 對於sell2而言,我們今天可以不做任何操作,直接通過昨天的sell2 轉移過來,或者,我們在buy2的前提下,今天把股票賣出。即

​ sell2=max(sell2,buy2+prices[i])

Tips:我們在計算sell1時,我們可以直接使用buy1而不是buy1進行轉移。buy1比buy1多考慮的是在第i天買入股票的情況,而轉移到sell1時,考慮的是在第i天賣出股票的情況,這樣在同一天買入並且賣出股票的收益為0,不會對結果產生影響。對於buy2和sell2也是同樣的考慮,我們可以直接用第i天求出的值來進行轉移。

buy1=max(buy1,-prices[i])
sell1=max(sell1,buy1+prices[i]
buy2=max(buy2,sell1-price[i])
sell2=max(sell2,buy2+prices[i])

我們來看程式碼實現:

def maxProfit(prices):
    n = len(prices)
    #表示以prices[0]買入股票
    buy1=-prices[0]
    #表示在同一天買入並且賣出,即為0
    sell1=0
    #表示同一天買入賣出後再以prices[0]的價格買入
    buy2=-prices[0]
    #表示同一天買入賣出兩次,即為0
    sell2 = 0
    for i in range(1, n):
        buy1 = max(buy1, -prices[i])
        sell1 = max(sell1, buy1 + prices[i])
        buy2 = max(buy2, sell1 - prices[i])
        sell2 = max(sell2, buy2 + prices[i])
    return sell2

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

最後

​ 對於像“多階段決策最優”問題來說,我們首先要考慮是否能用動態規劃來解決,看是否滿足動態規劃的三個特徵:重複子問題、最優子結構、無後效性。然後寫出狀態轉移方程,那問題基本就解決了。

​ 今天,我們就聊到這裡。更多有趣知識,請關注公眾號【程式設計師學長】。

​ 你知道的越多,你的思維也就越開闊,我們下期再見。

相關文章