遞迴與動態規劃---換錢的方法數
【問題】
給定陣列arr,arr中所有的值都為整數且不重複。每個值代表一種面值的貨幣,每種貨幣有無數張,再給定一個整數aim代表要找的錢數,求換錢的方法有多少種。
【基本思路】
這道題的經典之處在於它可以體現暴力遞迴、記憶搜尋、動態規劃之間的關係,並可以在動態規劃的基礎上再進行一次優化。
首先介紹暴力遞迴的方法。如果arr = [5, 10, 25, 1],aim = 1000,分析過程如下:
- 用0張5元的貨幣,讓[10, 25, 1]組成剩下的1000,最終方法數記為res1。
- 用1張5元的貨幣,讓[10, 25, 1]組成剩下的995,最終方法數記為res2。
- 用2張5元的貨幣,讓[10, 25, 1]組成剩下的990,最終方法數記為res3。
…… - 用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]的值求法如下:
- 對於矩陣的第一行,表示只使用貨幣arr[0]的情況下,組成錢的方法數,可以組成的錢數是arr[0]的整倍數,所以將其設定成1
- 對於矩陣的第一列,表示組成錢數0的方法數,很明顯是1種,也就是不使用任何貨幣,所以第一列都設定為1
- 對於矩陣的其他位置,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]