遞迴和動態規劃(一)
換錢的方法數
題目:給定陣列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. 用3張5元的貨幣,讓[10, 25, 1]組成剩下的990,最終方法數記為res3
...
201. 用200張5元的貨幣,讓[10, 25, 1]組成剩下的0,最終方法數記為201
那麼res1 + res2 + ...+res201 的值就是總的方法數。
具體程式碼實現如下:
解法二:記憶化搜尋int coins1(vector<int> arr, int aim) { if(arr.size() == 0 || aim < 0) return 0; return process1(arr, 0, aim); } int process1(vector<int> arr, int index, int aim) { int res = 0; if(index == arr.size()) res = aim == 0 ? 1 : 0; else for(int i=0; arr[index]*i <= aim; i++) res += process1(arr, index+1, aim - i*arr[index]); return res; }
針對暴力遞迴存在大量重複搜尋的情況,可以事先準備一個m,每計算完一個遞迴過程,都將結果記錄到m中,當下次進行同樣的遞迴過程之前,先m中查詢這個遞迴過程是否已經計算過,如果已經計算過,就把值拿出來直接用,如果沒有計算過,需要再進入遞迴過程。
m[i][j]表示遞迴過程p(i, j)的返回值。
m[i][j] = 0表示遞迴過程p(i, j)從來沒有計算過。
m[i][j] = -1 表示遞迴過程p(i, j)計算過,但返
回值是0.如果m[i][j]的值既不等於0,也不等於-1,記為a,則表示遞迴過程p(i, j)返回值為a
具體程式碼如下:
int coins2(vector<int> arr, int aim) { if(arr.size() == 0 || aim < 0) return 0; vector<vector<int> > m(arr.size()+1, vector<int>(aim+1, 0)); return process2(arr, 0, aim, m); } int process2(vector<int> arr, int index, int aim, vector<vector<int> >& m) { int res = 0; if(index == arr.size()) res = aim == 0 ? 1 : 0; else { int mapValue = 0; for(int i=0; i * arr[index] <= aim; i++) { mapValue = m[index+1][aim-i*arr[index]]; if(mapValue != 0) res += mapValue == -1 ? 0 : mapValue; else res += process2(arr, index+1, aim-i*arr[index], m); } } m[index][aim] = res == 0 ? -1 : res; return res; }
解法三:動態規劃
生成行數為N列數為aim+1的矩陣dp,dp[i][j]表示在使用arr[0...i]貨幣的情況下,組成錢數j的方法數。
- 對於dp第一列的值dp[...][0], 表示組成錢數0的方法數,只有一種,就是不使用任何貨幣
- 對於dp第一行的值dp[0][...],表示只使用arr[0]這一種貨幣的情況下,組成錢的方法數
- 其他位置(i, j)的dp值是以下幾個值的累加。
- 完全不用arr[i]貨幣,只使用arr[0...i-1]貨幣時,方法數為dp[i-1][j]
- 用1張arr[i]貨幣,剩下的錢用arr[0...i-1]貨幣組成時,方法數為dp[i-1][j-arr[i]]
- 用2張arr[i]貨幣,剩下的錢用arr[0...i-1]貨幣組成時,方法數為dp[i-1][j-2*arr[i]].
- 用k張arr[i]貨幣,剩下的用arr[0...i-1]貨幣組成時,方法數為dp[i-1][j-2*arr[i]].
- .............
具體程式碼如下:
int coins3(vector<int> arr, int aim)
{
if(arr.size() == 0 || aim < 0)
return 0;
vector<vector<int> > dp(arr.size(), vector<int>(aim+1, 0));
for(int i=0; i<arr.size(); i++)
dp[i][0] = 1;
for(int j=1; j*arr[0]<=aim; j++)
dp[0][arr[0] * j] = 1;
int num = 0;
for(int i=1; i<arr.size(); i++)
{
for(int j=1; j<=aim; j++)
{
num = 0;
for(int k=0; j - k*arr[i] >= 0; k++)
num += dp[i-1][j - k*arr[i]];
dp[i][j] = num;
}
}
return dp[arr.size()-1][aim];
}
解法四:動態規劃(改進解法三)
解法三,第1種情況的方法數就是dp[i-1][j], 而第2種情況一直到第k種情況的方法累加值其實就是dp[i][j-arr[i]]
則dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]
具體程式碼如下:
int coins4(vector<int> arr, int aim)
{
if(arr.size() == 0 || aim < 0)
return 0;
vector<vector<int> > dp(arr.size(), vector<int>(aim+1, 0));
for(int i=0; i<arr.size(); i++)
dp[i][0] = 1;
for(int j=1; j*arr[0]<=aim; j++)
dp[0][j*arr[0]] = 1;
for(int i=1; i<arr.size(); i++)
for(int j=1; j<=aim; j++)
{
dp[i][j] += dp[i-1][j];
dp[i][j] += j-arr[i] >= 0 ? dp[i][j-arr[i]] : 0;
}
return dp[arr.size()-1][aim];
}
解法五:動態規劃空間壓縮
int coins5(vector<int> arr, int aim)
{
if(arr.size() == 0 || aim < 0)
return 0;
vector<int> dp(aim+1, 0);
for(int j=1; j*arr[0]<=aim; j++)
dp[j*arr[0]] = 1;
for(int i=1; i<arr.size(); i++)
for(int j=1; j<=aim; j++)
dp[j] += j - arr[i] >= 0 ? dpj-arr[i]] : 0;
return dp[aim];
}
注:演算法思路參考程式設計師程式碼面試指南,由本人自己改由C++實現!