1. 程式人生 > >動態規劃案例(python版本)

動態規劃案例(python版本)

最近幾天一直在看有關動態規劃的演算法,整理了一些常見案例,主要是求最長公共子序列最長公共子串最長遞增子序列最長迴文子串硬幣的組合數硬幣的最少組合方法最小編輯距離揹包問題(01揹包,完全揹包,多重揹包)等方面的經典案例求解。

這些案例大部分都是用python實現的動態規劃演算法

案例一:求最長公共子序列(不一定連續)

Q:給定兩個序列,找出在兩個序列中同時出現的最長子序列的長度。一個子序列是出現在相對順序的序列,但不一定是連續的。

分析:

  • 假設str1的長度為M,str2的長度為N,生成的大小為M*N的矩陣dp。dp[i][j]的含義是str[0...i]與str2[0...j]的最長公共子序列的長度。
  • 矩陣dp第一列,即dp[i][0],代表str1[0...i]與str2[0]的最長公共子序列長度。str2[0]只有一個字元,所以dp[i][0]最大為1,如果str[i] == str2[0],則令dp[i][0]為1,一旦dp[i][0]被設為1,則令dp[i+1...M][0]全部為1
  • 矩陣dp第一行,即dp[0][j],與步驟1同理。如果str1[0]==str[j],則令dp[0][j]為1,一旦dp[0][j]被設為1,則令dp[0][j+1...N]全部為1
  • 其他位置,dp[i][j]的值只可能來自一下三種情況,三種可能的值中,選擇最大的值即可
  1.  情況一:可能是dp[i-1][j]的值,這代表str1[0....i-1]與str2[0...j]的最長公共子序列長度。    舉例:str1 = "A1BC2", str2 = "AB34C"    str1[0..3]為"A1BC",str2[0...4]為"AB34C",這兩部分最長公共子序列為"ABC",即dp[3][4]為3.      str1整體和str2整體最長公共子序列也是"ABC",所以dp[4][4]可能來自dp[3][4]
  2. 情況二:同理可知,dp[i][j]的值也可能是dp[i][j-1]
  3. 情況三:如果str1[i]==str2[j],還可能是dp[i-1][j-1]+1的值。    舉例:比如str1 ="ABCD", str2 = "ABCD". str1[0...2]即“ABC”與str2[0...2]即“ABC”的最長公共子序列為"ABC",也就是dp[2][2]為3。因為str1和str2的最後一個字元都是"D",所以dp[i][j] = dp[i-1][j-1]+1
程式碼:
def findLongest(self, A, n, B, m):  
    #新建一個m行n列的矩陣  
    matrix = [0] * m * n  
    #1、矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串.  
    # str2[0]只有一個字元,所以matrix[i][0]最大為1  
    for i in range(n):  
        if A[i] == B[0]:  
            for j in range(i,n):  
                matrix[j] = 1  
    #2、矩陣的第一列,matrix[i][0]最大為1  
    for i in range(m):  
        if B[i] == A[0]:  
            for j in range(i,m):  
                matrix[j*n] = 1  
    #3、其他位置,matrix[i][j]有三種情況,matrix[m][n]即為所求的最長公共子序列長度  
    for i in range(1,m):  
        for j in range(1,n):  
            if B[i] == A[j]:  
                matrix[i*n+j] = max(matrix[(i-1)*n+j-1]+1,matrix[(i-1)*n+j],matrix[i*n+j-1])  
            else:  
                matrix[i*n+j] = max(matrix[(i-1)*n+j],matrix[i*n+j-1])  
    return matrix[m*n-1]  

案例二:求最長公共子串(連續)

Q:給定兩個序列,找出在兩個序列中同時出現的最長子序列的長度。子串的意思是要求為連續的子序列

分析:

矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串.

與案例一中的前兩步相同,只是最後一步不同。

程式碼:

def findLongest(self, A, n, B, m):  
    #新建一個m行n列的矩陣  
    matrix = [0] * m * n  
    #1、矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串.  
    # str2[0]只有一個字元,所以matrix[i][0]最大為1  
    for i in range(n):  
        if A[i] == B[0]:  
            matrix[i] = 1  
    #2、矩陣的第一列,matrix[i][0]最大為1  
    for i in range(m):  
        if B[i] == A[0]:  
            matrix[i*n] = 1  
    #3、其他位置  
    max = 0  
    for i in range(1,m):  
        for j in range(1,n):  
            if B[i] == A[j]:  
                matrix[i*n+j] = matrix[(i-1)*n+j-1]+1  
                if max<matrix[i*n+j]:  
                    max = matrix[i*n+j]  
    return max  

案例三:最長遞增子序列

Q:給定一個序列,找到最長子序列的長度,使得子序列中的所有元素被排序的順序增加。比如arr = [2,1,5,3,6,4,8,9,7], 最長遞增子序列為[1,3,4,8,9],所以返回這個子序列的長度5。給定陣列arr,返回陣列arr,返回arr的最長遞增子序列長度。比如arr =[2,1,5,3,6,4,8,9,7],最長遞增子序列為[1,3,4,8,9],所以返回這個子序列的長度5

分析:

dp[i]表示在必須以arr[i]結尾的情況下,arr[0 ... i]中的最大遞增子序列長度,

dp[i] = max{ dp[j]+1  (0<=j<i , arr[j]<arr[i])}

程式碼:

def findLongest(self, A, n):
    dp = [0] * n
    dp[0] = 1
    for i in range(1, len(A)):
        l = [1]
        for j in range(0, i):
            if A[i] > A[j]:
                l.append(dp[j] + 1)
        dp[i] = max(l)
    return max(dp)

案例四:最長迴文子串

Q:給一個字串,找出它的最長的迴文子序列LPS的長度。例如,如果給定的序列是“BBABCBCAB”,則輸出應該是7,“BABCBAB”是在它的最長迴文子序列。

分析:

dp[i][j] = 1表示字串s從i到j是迴文串 dp[i][j] = 0表示字串s從i到j不是迴文串

如果dp[i][j] = 1, 那麼dp[i+1][j-1] = 1

程式碼:

    def manacher(self,s):
        #建立一個二維陣列
        maxlen = 0
        start = 0
        dp = [[0 for i in range(len(s))] for i in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1
            if i+1<len(s) and s[i] == s[i+1]:
                dp[i][i+1] = 1
                maxlen = 2
                start = i
        for i in range(3,len(s)+1):  #i表示迴文子串長度,從3開始,最長為len(s)
            for j in range(len(s)-i+1): #j表示指標移動的起點
                k = i+j-1 #k表示終點
                if dp[j+1][k-1]==1 and s[j]==s[k]:
                    dp[j][k] = 1
                    if i>maxlen:
                        start = j
                        maxlen = i
        if maxlen>=2:
            return s[start:start+maxlen]
        return None

案例五:硬幣最少數量(湊齊n元最少需要幾枚硬幣

Q:假設有 1 元,3 元,5 元的硬幣若干(無限),現在需要湊出 11 元,問如何組合才能使硬幣的數量最少?

分析:

我們先假設一個函式 d(i) 來表示需要湊出 i 的總價值需要的最少硬幣數量。

  1. 當 i = 0 時,很顯然我們可以知道 d(0) = 0。因為不要湊錢了嘛,當然也不需要任何硬幣了。注意這是很重要的一步,其後所有的結果都從這一步延伸開來
  2. 當 i = 1 時,因為我們有 1 元的硬幣,所以直接在第 1 步的基礎上,加上 1 個 1 元硬幣,得出 d(1) = 1
  3. 當 i = 2 時,因為我們並沒有 2 元的硬幣,所以只能拿 1 元的硬幣來湊。在第 2 步的基礎上,加上 1 個 1 元硬幣,得出 d(2) = 2
  4. 當 i = 3 時,我們可以在第 3 步的基礎上加上 1 個 1 元硬幣,得到 3 這個結果。但其實我們有 3 元硬幣,所以這一步的最優結果不是建立在第 3 步的結果上得來的,而是應該建立在第 1 步上,加上 1 個 3 元硬幣,得到 d(3) = 1
  5. ...

接著就不再舉例了,我們來分析一下。可以看出,除了第 1 步這個看似基本的公理外,其他往後的結果都是建立在它之前得到的某一步的最優解上,加上一個硬幣得到。得出:

d(i) = d(j)+1

這裡j<i。通俗的將,我們需要湊出 i 元,就在湊出 j 的結果上再加上某一個硬幣就行了。那這裡我們加上的是哪個硬幣呢。嗯,其實很簡單,把每個硬幣試一下就行了:

  1. 假設最後加上的是 1 元硬幣,那 d(i) = d(j) + 1 = d(i - 1) + 1
  2. 假設最後加上的是 3 元硬幣,那 d(i) = d(j) + 1 = d(i - 3) + 1
  3. 假設最後加上的是 5 元硬幣,那 d(i) = d(j) + 1 = d(i - 5) + 1

我們分別計算出d(i - 1) + 1,d(i - 3) + 1,d(i - 1) + 1的值,取其中的最小值,即為最優解,也就是d(i)。

最後公式

d(i) = min( d(i - 1) + 1,d(i - 3) + 1,d(i - 5) + 1 )

程式碼:

    def findLeast(self, n):
        # write code here
        l = [0,1,2,1,2,1]
        for i in range(6,n+1):
            l.append(min(l[i-1]+1,l[i-3]+1,l[i-5]+1))
        return l[n]

案例六:硬幣組合種類數(湊齊n分錢有多少種方法)

Q:有數量不限的硬幣,幣值為25分、10分、5分和1分,請編寫程式碼計算n分有幾種表示法。給定一個int n,請返回n分有幾種表示法。保證n小於等於1000,為了防止溢位,請將答案Mod 1000000007。

分析:

  1. dp[i][sum]表示用前i種硬幣構成sum的所有組合數,本題實際上就是求dp[n][sum]
  2. coins = [1,5,10,25]
  3. dp[i][sum] = dp[i-1][sum-0*coins[i]] + dp[i-1][sum-1*coins[i]]+....+dp[i-1][sum-k*coins[i]](k = sum/coins[i])
  4. 上一步化簡後:dp[i][sum] = dp[i-1][sum-k*coins[i]]求和 (k = 0...sum/coins)

程式碼:

    def coinsWays(self, n):
        coins = [1,5,10,25]
        dp = [[0 for i in range(n+1)] for i in range(5)]
        for i in range(5):
            dp[i][0] = 1
        for i in range(1,5):
            for j in range(1,n+1):
                for k in range(j/coins[i-1]+1):
                    dp[i][j] += dp[i-1][j-k*coins[i-1]]
        return dp[4][n]

案例七:最小編輯距離

Q:給定一個長度為m和n的兩個字串,設有以下幾種操作:替換(R),插入(I),刪除(D)且都是相同代價的操作。尋找到轉化一個字串插入到另一個需要修改的最小(操作)數量。

分析:

  1. dp[i][j] 表示長度為i的字串A替換到長度為j的字串B所付出的代價
  2. 當兩個字串的大小為0,其操作距離為0。
  3. 當其中一個字串的長度是零,需要的操作距離就是另一個字串的長度. 


程式碼:

    def editDist(self,s1,s2):
        #思路:
        #dp[i][j] 表示長度為i的字串A替換到長度為j的字串B所付出的代價
        len1 = len(s1)
        len2 = len(s2)
        dp = [[0 for i in range(len2+1)]for i in range(len1+1)]
        for i in range(len1+1):
            dp[i][0] = i
        for i in range(len2+1):
            dp[0][i] = i
        for i in range(1,len1+1):
            for j in range(1,len2+1):
                #如果當前兩個字串指標所指向的字元相等時,
                if s1[i-1]==s2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1
        return dp[len1][len2]

案例八:揹包問題(01揹包,完全揹包,多重揹包

這裡只寫出了c++的寫法

首先分別解釋一下三種揹包的含義

  • 01揹包:有n種物品與承重為m的揹包。每種物品只有一件,每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大
  • 完全揹包:有n種物品與承重為m的揹包。每種物品有無限件,每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大
  • 多重揹包:有n種物品與承重為m的揹包。每種物品有有限件num[i],每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大

關於01揹包:

為什麼叫01揹包,因為裝進去就是1,不裝進去就是0,所以針對每個物品就有兩種狀態,裝?不裝?所以這個揹包只要有足夠大的空間,這個物品都是有可能被裝進去的。

所以有狀態轉移方程

dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])

for (i = 1; i <= n; i++)   #從1開始是因為這涉及到dp[i-1][j],從0開始會越界

    for (m = v; j >= weight[i]; j--)//在這裡,揹包放入物品後,容量不斷的減少,直到再也放不進了

       dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])

仔細分析就會發現,這種二維陣列開銷很大,因此有了下面的滾動陣列,說白了只是把所有的物品都跑一遍,然後到最後一個物品的時候輸出答案,那麼過程值只是計算的時候用一次,沒必要存下來,所以用一個數組去滾動儲存,然後用後一個狀態的值去覆蓋前一個狀態。

    for(int i=1; i<=n; i++)//對每個數判斷,可反  
    {  
        for(int j=m; j>=weight[i]; j--)//這裡這個迴圈定死,不能反,反了就是完全揹包  
        {  
            dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//其實不斷在判斷最優解,一層一層的  
        }  
    }  
其實就是規定從m開始迴圈,保證了選擇這個物品時,肯定不會重複使用狀態。

關於完全揹包:

完全揹包每個物品都是無限,認死了選價效比最高的,不一定是完全填滿揹包的。(其實就是01揹包一維陣列中把j倒置)

這裡的二維陣列就不如一維陣列了

for(int i=0;i<n;i++){

          for(int j=node[i].b;j<=m;j++){//這樣就是完全揹包

               dp[j]=max(dp[j],dp[j-node[i].b]+node[i].a)
關於多重揹包:

首先把物品拆開,把相同的num[i]件物品看成價值和重量相同的num[i]件不同的商品,那麼,就轉化成了一個規模稍微大一點的01揹包了。

   for(int i=1; i<=n; i++)//每種物品
       for(int k=0; k<num[i]; k++)//其實就是把這類物品展開,呼叫num[i]次01揹包程式碼
           for(int j=m; j>=weight[i]; j--)//正常的01揹包程式碼
               dp[j]=max(dp[j],dp[j-weight[i]]+value[i])

以上八種案例為動態規劃的經典案例,後序還會進行不定期更新!