算法 | Five Steps to Dynamic Programming(解決動態規劃問題的五個步驟)

動態規劃(DP)是最常用的算法之一。它藉助Divide and Conquer的思想,將一個問題分解爲一個個漸進的subproblems(子問題),最終通過解決這些子問題來得到問題的終極答案。
和recursion(遞歸)的不同,DP會將所有子問題的答案存在一個表格裏,以達到方便提取、提升效率的作用。因此,用遞歸需要指數時間解決的問題,很多都可以用DP在多項式時間內解決。一個最常見的例子就是計算第n項斐波那契數列

解決動態規劃問題的五個步驟如下:

一、判斷問題是否能用動態規劃算法解決

一般來說,動態規劃問題都可以寫成如下形式:
maxf(x1,x2,...xn)s.t.L(y1,y2,...,yn)<0 \max f(x1,x2,...xn) s.t. L(y1,y2,...,yn)<0
where L(y1,y2…yn) is the constraint function
即,在某些限制條件之下求出目標函數(objective function)的最大值或最小值
Max Array Sum問題來舉例說明:輸入一個數列arr,這道題的目標是在arr中選擇若干兩兩不相鄰的數,使它們的和最大。這道題就是典型的動態規劃,限制條件是必須選擇不相鄰的數,目標函數則是它們的和。

二、寫出子問題(subproblem)

將目標函數表示爲OPT(n),那麼子問題一般可以表示爲OPT(k),其中k是input的大小。
繼續以Max Array Sum問題爲例。input長度爲n的數列arr,我們最終想要算出的是:
OPT(n)=arr滿 OPT(n) = arr中滿足限制條件的數之和的最大值
子問題可以表示爲:
OPT(k)=arr[0:k]滿 OPT(k) = arr[0:k]中滿足條件的數之和的最大值
也就是說,子問題解決的是arr前k項的Max Array Sum問題。

三、寫出Recurrence(遞歸函數)

假設我們知道OPT(1)~OPT(k)的答案,如何用它們算出OPT(k+1)?答案是遞歸函數,連接子問題和目標函數之間的橋樑。 遞歸函數的一般形式是:
OPT(k+1)=f(OPT(1),OPT(2),...,OPT(k)) OPT(k+1) = f( OPT(1),OPT(2),...,OPT(k) )
找到遞歸函數是動態規劃中最重要,也是最有難度的一步。我們一般需要通過分類討論來窮盡所有可能性。
在Max Array Sum中,OPT(k+1)將考慮的數列從前k項增加爲前k+1項。這時,有兩種可能性:1)數列第k+1項不在最優結果裏;2)數列第k+1項在最優結果裏。如果是前者,那麼OPT(k+1) = OPT(k);如果是後者,那麼既然第k+1項在最優結果裏,根據限制條件,第k項就不會在最優結果裏。也就是說,此時OPT(k+1)與OPT(k-1)和arr[k]有關。這時又有兩種可能性:1)OPT(k+1) = OPT(k-1) + arr[k];2)OPT(k-1)是負數,因此OPT(k+1)直接等於arr[k]。
總結以上分析,我們可以寫出這道題的遞歸函數:
OPT(k+1)=maxOPT(k),OPT(k1)+arr[k],arr[k]) OPT(k+1) = \max OPT(k), OPT(k-1)+arr[k], arr[k] )
即,選擇三種可能性中的最大值作爲最優解。

四、確定Base Case(終止條件)

和遞歸一樣,寫出程序停止的條件。一般而言這個條件都是取n=1。
Max Array Sum中的base case顯而易見:
OPT(1)=arr[0]OPT(2)=max(arr[0],arr[1])OPT(1) = arr[0],OPT(2) = \max(arr[0],arr[1])

五、代碼實現

代碼實現有兩種思路:Memoization和Tabulation。
簡單理解,memoization等於top down approach(從上至下):我要計算OPT(n),因此需要算出OPT(1), OPT(2),… 這個思路類似遞歸。而tabulation則是bottom up approach(由下至上):第一步算出OPT(1),再算出OPT(2),…最後算出OPT(n)。
從邏輯角度,memoization更加直接易懂,但是從效率角度我個人更喜歡tabulation。memoization在執行過程中會積壓大量指令,因此在n較大的時候可能會造成較高的空間浪費。
Max Array Sum問題採取tabulation的代碼實現如下:

#arr is the input array
def maxSubsetSum(arr):
    k=len(arr)
    dic={}
    dic[1]=arr[0]            #Base case
    if k>1:
        dic[2]=max(arr[0],arr[1])    
        for k in range(3,len(arr)+1):
            dic[k]=max(dic[k-1],dic[k-2]+arr[k-1],arr[k-1])    #Recurrence
    return dic[k] 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章