CCF-訓練50題-NO.29-最少錢幣數
一、問題描述
這是一個古老而又經典的問題。用給定的幾種錢幣湊成某個錢數,一般而言有多種方式。例如:給定了 6 種錢幣面值為 2、5、10、20、50、100,用來湊 15 元,可以用 5 個 2 元、1個 5 元,或者 3 個 5 元,或者 1 個 5 元、1個 10 元,等等。顯然,最少需要 2 個錢幣才能湊成 15 元。你的任務就是,給定若干個互不相同的錢幣面值,程式設計計算,最少需要多少個錢幣才能湊成某個給出的錢數。
二、問題分析
硬幣問題就是一個多重揹包問題
動態遷移方程為 (注:t表示可以使用的錢幣的面額組成的陣列,d[k]表示要湊k元最少需要多少種的面額的紙幣)
d[k] = min{d[k-t[i]]+1,d[k]}
就是,將第i個硬幣拿出去得到的一個最少的找硬幣數+1,和原硬幣數相比最小的那個就是結果
三、演算法分析
可以使用遞迴做,也可以用0/1揹包問題的動態規劃做。遞迴的核心程式碼是表示,每一個值的最少情況都算出來,而動態規劃是用迴圈體來找出最優解。
遞迴:
return min(SE_min(a,n,s,i+1),SE_min(a,n,s-a[i],i)+1);
動態規劃:
dp[k] = min{dp[k-t[i]]+1,dp[k]}
四、詳細設計(遞迴方法)
#include <iostream>
using namespace std;
int min(int a,int b){
return a<b?a:b;
}
int SE_min(int *a,int n,int s,int i=0){
if(s==0){
return 0;
}
(一)每次的第一個元素就不用再找了。
for(int z=0;z<n-1;z++){
if(a[z]>a[z+1]){
int p=a[z];
a[z]=a[z+1];
a[z+1]=p;
}
}
(二)當所有的錢幣都用過了,而且所有組合都試過了,都沒有解,就表示不可能。
if(i>=n||s<a[i]){
return 100000;
}
(三)關鍵語句,不停呼叫函式,直到最後是找到第一個可以湊到的錢。
return min(SE_min(a,n,s,i+1),SE_min(a,n,s-a[i],i)+1);
}
int main(){
int n,w;
while (1){
cin>>w;
if (w==0) break;
cin>>n;
int *p=new int[n];
for (int i=0;i<n;i++) cin>>p[i];
(四)呼叫遞迴。
if (SE_min(p,n,w,0)>=n) cout<<"Impossible";
else cout<<SE_min(p,n,w,0);
}
return 0;
}
五、詳細設計(動規方法)
#include<iostream>
using namespace std;
int main()
{
int n,m;
while (cin>>m&&m){
cin>>n;
注意:這裡確定所需的元素個數,並且申請存放地址,這裡在oj系統上會有報錯提示,這裡應該申請一個有限大小的常規陣列。
int *t=new int[n+1];
int *coin=new int[n+1];
(一)輸入資料,每個面額的錢的數目為要湊的錢除以其面額。
for(int i=1;i<n+1;i++){
cin>>t[i];
coin[i]=(m/t[i]);
}
int d[2002]={0};
for (int i=1;i<=m;i++) d[i]=99999;
for(int i=1;i<=n;i++)
for(int j=1;j<=coin[i];j++)
for(int k=m;k>=t[i];k--){
(二)利用狀態轉移方程,計算最小值。
d[k]=min(d[k-t[i]]+1,d[k]);
}
(三)如果找不到何時的湊錢方法,d[m]的值是不會被改變的。
if (d[m]==99999) cout<<"Impossible"<<endl;
else cout<<d[m]<<endl;
}
}
六、分析與總結
這題目是基礎的演算法題,對於這道經典的多重揹包問題,在這50道題裡面,屢見不鮮,說明這個問題的重要性,是必須要掌握的。而遞迴的方法是最直觀最好想到的,但是在debug初期一直沒有成功是因為對於金幣的數列我沒有做到使用完就刪掉的方法,這樣對於後面遞迴的引數來說,首地址一直都是第一個元素的地址,這樣的解題肯定會出錯誤。而動態規劃問題,只要找到狀態轉移方程,就顯而易見,迎刃而解。所以這一題告訴我要熟練掌握揹包問題以及類似這類問題的動態規劃的解法。學好演算法,任重道遠。