硬幣找零問題之動態規划


今天我們看一下動態規划的硬幣找零問題,主要通過一系列編程題分析動態規划的規律,只要掌握這一規律,許多動態規划的相關問題都可以類比得到。

題目1:給定數組arr,arr中所有的值都是正數且不重復。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim代表要找的錢數,求組成aim的最少貨幣數。

舉例: 

arr[5,2,3],aim=20。  4張5元可以組成20元,其他的找錢方案都要使用更多張的貨幣,所以返回4。

題解:

一眼看去這道題好像可以用貪心算法可解,但是仔細分析發現有些值是不可以的,例如arr[1,3,4],aim=6 :用貪心算法算最少錢數為3 (4+1+1),但是我們可以明顯的發現用兩張3元的就夠了,所以用貪心算法不可解。

其實這是一道經典的動態規划方法,我們可以構造一個dp數組,如果arr的長度為N,則dp數組的行數為N,列數為aim+1,dp[i][j] 的含義是:在可以任意使用arr[0..i]貨幣的情況下,組成j所需要的最小張數。

明白以上定義后我們初始化第一行與第一列,第一行dp[0][0..aim]中每一個元素dp[0][j]表示用arr[0]貨幣找開面額 j所需要的最少貨幣數,此時我們只能選取arr[0]這一張貨幣,所以只有arr[0]的整數倍的面額錢才可以找開,例如當arr[0]=3,aim=10時,只能找開3,6,9的貨幣,而其他面額的則無法找開,所以將arr[0][3,6,9]初始化為1,2,3 除此之外其他值初始化為整形int的最大值INT_MAX表示無法找開。對於第一列dp[0..n][0] 中的每一個元素dp[i][0]表示用arr[i]組成面額為0的錢的最少貨幣數,完全不需要任何貨幣,直接初始化為0即可。

     對於剩下的任意dp[i][j],我們依次從左到右,從上到下計算,dp[i][j]的值可能來自下面:

  • 完全不使用當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j]的值
  • 只使用1張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-arr[i]]+1
  • 只使用2張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-2*arr[i]]+2
  • 只使用3張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-3*arr[i]]+3
  • …..

以上所有情況中,最終取張數最小的,即dp[i][j] = min( dp[i-1][j-k*arr[i]]+k )( k>=0 )

=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-x*arr[i]]+x (1<=x) } }    令x = y+1

=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-arr[i]-y*arr[i]+y+1 (0<=y) ] } } 

又有 min{ dp[i-1][j-arr[i]-y*arr[i]+y (0<=y) ] } => dp[i][ j-arr[i] ] ,所以,最終有:dp[i][j] = min{ dp[i-1][j], dp[i][j-arr[i]]+1 }。如果j-arr[i] < 0,即發生了越界,說明arr[i]太大了,用一張都會超過錢數j,此時dp[i][j] = dp[i-1][j]。

int coinChange(vector<int>& arr, int aim) { int len = arr.size(); vector<vector<int>> dp(len, vector<int>(aim+1,0)); for(int i=1; i<=aim; i++){ dp[0][i] = INT_MAX; if( i>=arr[0] && dp[0][i-arr[0]] !=INT_MAX ){ dp[0][i] = dp[0][i-arr[0]]+1; } } for(int i=1; i<len; i++){ for(int j=1; j<=aim; j++){ int left = INT_MAX; if( j>=arr[i] && dp[i][j-arr[i]] != INT_MAX ){ left=dp[i][j-arr[i]]+1; } dp[i][j] = min( dp[i-1][j], left ); } } return dp[len-1][aim]==INT_MAX ? -1 : dp[len-1][aim]; }

 

參見leetcode : 322. Coin Change 

上面的問題還可以進行空間壓縮,此處不再贅述。

上面說貪心算法不可解其實面值在部分情況下可以用貪心算法解決,如果可換的硬幣的單位是 c 的冪,也就是 c0,c1,... ,ck ,其中整數 c>1,k>=1,一定可以用貪心算法,或者某些情況比如,面值為1,5,10,20,50,100時,貪心找零也一定有最優解。參見《挑戰程序設計競賽》第二版2.2.1節。

 

題目2: 給定數組arr,arr中所有的值都為正數,每個值僅代表一張錢的面值,再給定一個整數aim代表要找的錢數,求組成aim的最小貨幣數。

題解: 相對於上一題,這道題的arr中的錢只有一張,而不是任意多張,構造dp數組的含義也同上,但是此時略有不同,

dp第一行dp[0][0..aim]的值表示只使用一張arr[0]貨幣的情況下,找某個錢數的最小張數。比如arr[0]=2,那么能找開的錢數僅為2, 所以令dp[0][2]=1。因為只有一張錢,所以其他位置所代表的錢數一律找不開,一律設為INT_MAX。第一列dp[0…N-1]表示找的錢數為0時需要的最少張數,錢數為0時完全不需要任何貨幣,所以全設為0即可。

剩下的位置從左到右,從上到下計算,dp[i][j]可能的值來自於以下兩種情況

  1. dp[i][j]的值代表在可以任意使用arr[0..i]貨幣的情況下,組成j所需要的最小張數。可以任意使用arr[0..i]貨幣的情況當然包括不使用arr[i]的貨幣,而只使用任意arr[0..i-1]貨幣的情況,所以dp[i][j]的值可能為dp[i-1][j]。
  2. 因為arr[i]只有一張不能重復使用,所以我們考慮dp[i-1][j-arr[i]]的值,這個值代表在可以任意使用arr[0..i-1]貨幣的情況下,組成j-arr[i]所需的最小張數。從錢數為j-arr[i]到錢數j,只用在加上這張arr[i]即可。所以dp[i][j]的值可能等於do[i-1][j-arr[i]]+1。
  3. 如果dp[i-1][j-arr[i]]中j-arr[i] < 0,也就是位置越界了,說明arr[i]太大了,只用一張就會超過錢數j,令dp[i][j]=dp[i-1][j]即可。
int coinChange(vector<int>& arr, int aim) { int len = arr.size(); vector<vector<int>> dp(len, vector<int>(aim+1,0)); for(int i=1; i<=aim; i++){ dp[0][i] = INT_MAX; } if ( arr[0] <= aim ){ dp[0][arr[0]] = 1; } for(int i=1; i<len; i++){ for(int j=1; j<=aim; j++){ int leftup = INT_MAX; if( j>=arr[i] && dp[i-1][j-arr[i]] != INT_MAX ){ leftup=dp[i-1][j-arr[i]]+1; } dp[i][j] = min( dp[i-1][j], leftup ); } } return dp[len-1][aim]==INT_MAX ? -1 : dp[len-1][aim]; }

 

 

題目3:給定數組arr,arr中所有的值都為正數且重復。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim代表要找的錢數,求換錢有多少種方法。 

題解: 類比題目1,每個面值的錢可以使用任意多次,我們可以構造一個dp數組,如dp數組的行數為N,列數為aim+1,dp[i][j] 的含義是:在可以任意使用arr[0..i]貨幣的情況下,組成錢數j有多少張方法。。

第一行dp[0][0..aim]中每一個元素dp[0][j]表示用arr[0]貨幣找開面額 j的方法,此時我們只能選取arr[0]這一張貨幣,所以只有arr[0]的整數倍的面額錢才可以找開,例如當arr[0]=3,aim=10時,只能找開3,6,9的貨幣,且只有一種方法即只是用arr[0],而其他面額的則無法找開,所以將arr[0][3,6,9]初始化為1 除此之外其他值初始化為0表示無法找開。對於第一列dp[0..n][0] 中的每一個元素dp[i][0]表示用arr[i]組成面額為0的錢的最少貨幣數,完全不需要任何貨幣,即一種方法,初始化為1。

      對於剩下的任意dp[i][j],我們依次從左到右,從上到下計算,dp[i][j]的值下面的方法數的和:

  • 完全不用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-k*arr[i]]

其實從第二種情況到第k種情況方法的累加值其實就是dp[i][j-arr[i]]的值,所以dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]] 。

 int countWays(vector<int> arr, int n, int aim) { vector<vector<int>> dp( n, vector<int>(aim+1,0) ); for(int j=0; arr[0]*j<=aim; j++){ dp[0][arr[0]*j] = 1; } for (int i = 1; i<n; i++){ dp[i][0] = 1; for (int j = 1; j <= aim; j++){ if (j - arr[i] >= 0) dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]]; else dp[i][j] = dp[i - 1][j]; } } return dp[n - 1][aim]; }

 

     

題目4:給定數組arr,arr中所有的值都為正數且重復。每個值代表一張錢的面值再給定一個整數aim代表要找的錢數,求換錢有多少種方法。

題解:類比題目2,也是每個錢只能使用一次,此處不做解釋

給出一道例題及答案:

例題:

鏈接:https://www.nowcoder.com/questionTerminal/7f24eb7266ce4b0792ce8721d6259800

來源:牛客網

給定一個有n個正整數的數組A和一個整數sum,求選擇數組A中部分數字和為sum的方案數。當兩種選取方案有一個數字的下標不一樣,我們就認為是不同的組成方案。

輸入描述:  輸入為兩行: 第一行為兩個正整數n(1 ≤ n ≤ 1000),sum(1 ≤ sum ≤ 1000),第二行為n個正整數A[i](32位整數),以空格隔開。

輸出描述:  輸出所求的方案數

輸入例子:

5 15

5 5 10 2 3 

輸出例子:

4

#include <iostream> #include <vector> #include <algorithm> #include <limits.h>
using namespace std; int main(){ long long a, sum; while (cin >> a >> sum){ vector<int> vec(a); for (int i = 0; i<a; i++)  cin >> vec[i]; vector<vector<long>> dp(a, vector<long>(sum + 1, 0)); for (int i = 0; i < a; i++){ dp[i][0] = 1; } if (sum >= vec[0]){ dp[0][vec[0]] = 1; } for (int i = 1; i<a; i++){ for (int j = 1; j <= sum; j++){ if (j >= vec[i]){ dp[i][j] = dp[i - 1][j] + dp[i - 1][j - vec[i]]; } else { dp[i][j] = dp[i - 1][j]; } } } cout << dp[a - 1][sum] << endl; } } 

 

進一步的優化交給讀者自己思考咯。

 

參考《程序員代碼面試指南》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM