1. 程式人生 > >遞迴與動態規劃---換錢的方法數

遞迴與動態規劃---換錢的方法數

【問題】

  給定陣列arr,arr中所有的值都為整數且不重複。每個值代表一種面值的貨幣,每種貨幣有無數張,再給定一個整數aim代表要找的錢數,求換錢的方法有多少種。

【基本思路】

這道題的經典之處在於它可以體現暴力遞迴、記憶搜尋、動態規劃之間的關係,並可以在動態規劃的基礎上再進行一次優化。

首先介紹暴力遞迴的方法。如果arr = [5, 10, 25, 1],aim = 1000,分析過程如下:

  1. 用0張5元的貨幣,讓[10, 25, 1]組成剩下的1000,最終方法數記為res1。
  2. 用1張5元的貨幣,讓[10, 25, 1]組成剩下的995,最終方法數記為res2。
  3. 用2張5元的貨幣,讓[10, 25, 1]組成剩下的990,最終方法數記為res3。
    ……
  4. 用201張5元的貨幣,讓[10, 25, 1]組成剩下的0,最終方法數記為res201。

那麼res1 + res2 + res3 + …… +res201的值就是中的方法數。根據如上的分析過程定義遞迴函式process1(arr, index, aim)它的含義是如果用arr[index..N-1]這些面值的錢組成aim,返回總的方法數。最壞情況下時間複雜度為O(aim^N),N表示陣列的長度。

下面是用python3實現的程式碼

#暴力遞迴方法
def coins1(arr, aim):
    def process1(arr, index, aim):
        if index == len(arr):
            return
1 if aim == 0 else 0 else: res = 0 for i in range(0, aim//arr[index]+1): res += process1(arr, index+1, aim-arr[index]*i) return res if arr == None or len(arr) == 0 or aim < 0: return 0 return process1(arr, 0, aim)

記憶搜尋的方法。在暴力遞迴中,有很多的重複計算,比如使用0張5元+1張10元的情況和使用2張5元+0張10元的情況,都需要求[25, 1]組成剩下的990的方法數。記憶搜尋就是使用一張記錄表將遞迴過程中的結果進行記錄,當下次再遇到同樣的遞迴過程,就直接使用表中的資料,這樣就對暴力遞迴進行了優化。
時間複雜度為O(N*aim^2),空間複雜度O(N*aim)。

#記憶搜尋方法
def coins2(arr, aim):
    def process2(arr, index, aim, records):
        if index == len(arr):
            return 1 if aim == 0 else 0
        else:
            res = 0
            for i in range(0, aim//arr[index]+1):
                mapValue = records[index+1][aim-arr[index]*i]
                if mapValue != 0:
                    res += mapValue if mapValue != -1 else 0
                else:
                    res += process2(arr, index+1, aim-arr[index]*i, records)
        records[index][aim] = -1 if res == 0 else res
        return res


    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    records = [[0 for i in range(aim+1)] for j in range(len(arr)+1)]
    return process2(arr, 0, aim, records)

動態規劃的方法。首先生成行數為N、列數為aim+1的dp矩陣,dp[i][j]的含義是在使用arr[0…i]貨幣的前提下,組成錢數j有多少種方法。dp[i][j]的值求法如下:

  1. 對於矩陣的第一行,表示只使用貨幣arr[0]的情況下,組成錢的方法數,可以組成的錢數是arr[0]的整倍數,所以將其設定成1
  2. 對於矩陣的第一列,表示組成錢數0的方法數,很明顯是1種,也就是不使用任何貨幣,所以第一列都設定為1
  3. 對於矩陣的其他位置,dp[i][j]的值來自以下幾個值的累加:完全不使用貨幣arr[i],使用一張貨幣arr[i],使用兩張貨幣arr[i]…使用k張貨幣arr[i](k=aim//arr[i]),對應到dp表裡分別是[i-1][j]、dp[i-1][j-arr[i]]、dp[i-1][j-2*arr[i]]…dp[i-1][j-k*arr[i]]。

時間複雜度為O(N*aim^2),空間複雜度O(N*aim)。

#動態規劃方法
def coins3(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    row = len(arr)
    dp = [[0 for i in range(aim+1)]for j in range(row)]
    for i in range(row):
        dp[i][0] = 1
    for j in range(1, aim//arr[0]+1):
        dp[0][arr[0]*j] = 1
    for i in range(1, row):
        for j in range(1, aim+1):
            num = 0
            for k in range(j//arr[i]+1):
                num += dp[i-1][j-arr[i]*k]
            dp[i][j] = num
    return dp[row-1][aim]

對動態規劃進行優化。上述動態規劃中計算dp[i][j]的過程比較繁瑣,其實計算dp的第三步可以進行優化。
在上述動態規劃的第三步中,dp[i][j] = dp[i-1][j] + dp[i-1][j-arr[i]] + dp[i-1][j-2*arr[i]] + …dp[i-1][j-k*arr[i]]。我們發現,這個等式中除了第一項以外其他項的累加和其實就是dp[i][j-arr[i]],只不過是將j-arr[i]這個整體當作j,所以步驟3可以優化為dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]。

時間複雜度為O(N*aim),空間複雜度O(N*aim)。

#動態規劃升級版
def coins4(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    row = len(arr)
    dp = [[0 for i in range(aim+1)] for j in range(row)]
    for i in range(row):
        dp[i][0] = 1
    for j in range(1, aim//arr[0]+1):
        dp[0][arr[0]*j] = 1
    for i in range(1,row):
        for j in range(1, aim+1):
            dp[i][j] = dp[i-1][j]
            dp[i][j] += dp[i][j-arr[i]] if j-arr[i] >= 0 else 0
    return dp[row-1][aim]

對動態規劃進行再優化。即使用空間壓縮的方法,優化動態規劃的空間複雜度,只是用一維陣列記錄資料而不是矩陣。
時間複雜度為O(N*aim),空間複雜度O(aim)。

#動態規劃升級版+空間壓縮
def coins5(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    dp = [0 for i in range(aim+1)]
    for i in range(aim//arr[0]+1):
        dp[arr[0]*i] = 1
    for i in range(len(arr)):
        for j in range(1, aim+1):
            dp[j] += dp[j-arr[i]] if j-arr[i] >= 0 else 0
    return dp[aim]