- 5. 最少钱币数
【问题描述】这是一个古老而又经典的问题。用给定的几种钱币凑成某个钱数,一般而言有多种方式。例如:给定了 6 种钱币面值为 2、5、10、20、50、100,用来凑 15 元,可以用 5 个 2 元、1个 5 元,或者 3 个 5 元,或者 1 个 5 元、1个 10 元,等等。显然,最少需要 2 个钱币才能凑成 15 元。
你的任务就是,给定若干个互不相同的钱币面值,编程计算,最少需要多少个钱币才能凑成某个给出的钱数。
【输入形式】输入可以有多个测试用例。每个测试用例的第一行是待凑的钱数值 M(1 <= M<= 2000,整数),接着的一行中,第一个整数 K(1 <= K <= 10)表示币种个数,随后是 K个互不相同的钱币面值 Ki(1 <= Ki <= 1000)。输入 M=0 时结束。
【输出形式】每个测试用例输出一行,即凑成钱数值 M 最少需要的钱币个数。如果凑钱失败,输出“Impossible”。你可以假设,每种待凑钱币的数量是无限多的。
【样例输入】
15
6 2 5 10 20 50 100
1
1 2
0
【样例输出】
2
Impossible
初看这个问题,我们并没有多少思路,假使我们确定一个记录每个面额的钱币用了多少的数组,那么这个问题是不是有点像全排列问题了,这个时候我们就需要用到回溯法来进行答案搜索了。这里,我们的DFS进行了剪枝。这里讲一下剪枝,对于我们的DFS的搜索,大家其实很想知道它的搜索路径是什么,很明显,深度优先形成一个茎,然后回溯,继续深度优先,那么最后就会形成一颗树,我们为了使这棵树看上去精干一点,就要剪去那些明显是错误的枝条,在这个题目中,如果我们使用的钱币的数量都大于我们之前搜索到的答案了,那么肯定就不得行了,无论它进行到什么程度,反正可以退出了,还要就是,搜素到的值都比要找的m都大的了,哪再怎么凑也不行了,所以说,也要去掉。
#include<iostream>
#include<cmath>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
int book[10001];
//int see[1000];
int arr[10001];
int ans=1000000;int m;
void dfs(int n,int ct,int k){
/*for(int i=1;i<=k;i++){
cout<<see[arr[i]]<<' ';
}
cout<<endl;*/
if(ct>ans){
return;
}
if(n>m){
return;
}
if(n==m){
ans=min(ans,ct);
}
for(int i=1;i<=k;i++){
int nn=n+arr[i];
if(nn>m){
continue;
}
if(book[nn]==0){
book[nn]=1;
//++see[arr[i]];
dfs(nn,ct+1,k);
//--see[arr[i]];
book[nn]=0;
}
}
}
int main() {
while(cin>>m){
if(m==0){
return 0;
}
int k;
cin>>k;
//int* dp=new int[m+1];
for(int i=1;i<=k;i++){
int ki;
cin>>ki;
int ct=0;
arr[i]=ki;
/*for(int j=ki;j<m;j*=ki){
dp[j]=++ct;
}*/
}
dfs(0,0,k);
if(ans==1000000){
cout<<"Impossible"<<endl;
} else {
cout<<ans<<endl;
}
ans=1000000;
//dp[n]=dp[n-arr[i]]+dp[arr[i]];
}
return 0;
}
通过上面的暴力搜索,我们得到了启发,因为如果凑到了一个较大的面额,实际上有很多种路径可以达到它,在那个实例中,比如n=15,我的dfs路径可以是3个5,1个10,2个5,也可以是5个2,1个5,随你怎么走,我们这道题要得到的东西其实就是个钱币数量就好了。知道这个了,我们就可以从子问题下手,保持了子问题的最优解,再想办法经由它递推得到原问题的最优解,岂不美哉?其实我们可以把它抽象为最短路问题,最少的钱币数代表了最少的距离,每段距离的权值是1,并且,咱们的图是一颗深度优先搜索树,你从一个节点到下一个节点有很多条路径,我们 只用找到最短的就好了,我们建立一个数组DP[m+1],其中DP[m]是面额为m时的最优解,那么它可以由什么节点得到呢?那就是将m减去某个面额的值,就是咱们的DP[m-arr[i]]!这是如何想到的呢?很显然,我们之前的那个容量对应的最优解,我把那个容量叫做m1,那么如果容量变为m1+某一个面额,是不是咱们对应的最短路加1就好了,由于我们无法确定这里由m1得到的最短路最短,还是m2,m3,m4...得到的最短路最短,所以说这个时候,我们要更新他的最小值,来获得容量为m时的最优解。
别急,我们还有一个重要的问题没有考虑,我之前 说的是某一个面额,其实也就是说,面额是乱序的也没有关系,那么咱们的最短路径,也就是金币数量呢?咱们的最优解是由小的值更新的,所以说容积要从小到大依次遍历。
咱们一开始先把数组每一个元素初始化为一个很大的值,建议设置一个常数,之后,开始更新,先考虑第一个面额,开始更新,如图所示
DP更新的过程
咱们的状态转移方程就是
这张图的箭头就是影响当前节点的点了。不难看出,这个图的更新方法像不像那个东西,没错就是Floyd多元最短路算法了,咱们的这个方法和弗洛伊德算法本质上是一样的,都是通过增加考虑的点数来转移咱们的最小值。
我们其实发现了,咱们的动态规划为什么省事,那就是因为,后面的问题跟前面的问题相关,而前面的问题已经算好了,可以直接取用,不像咱们的暴力搜索,要重复算很多东西,那个ct的值变来变去的,但是最后殊途同归,其实做了很多无用功,为什么呢,因为有些路径对应的最优解咱们其实之前算过了,但是根据DFS的特性,还要走一次,这个缺陷是剪枝也避免不了的,除非使用记忆化搜索,但是我懒得写记忆化搜索了,理论上来说,写出那个DFS,记忆化搜索还是比较好改的,我有个思路,就是也是加一个数组来存储对应子问题的最优解,然后多加一个参数来维护都已经使用了哪些节点,接下来递归成更小的子问题就好了,总体上应该和DFS差不太多了,就是把它改成返回值的函数,然后令dp[n]=dfs(n),这样以后再递归到n这个容量的时候,就不用继续递归了。
#include<iostream>
#include<cmath>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
static const int bn=100000000;
int dp[2001];
int arr[11];
int minval(int a,int b){
if(a==bn+1)
a= bn;
if(b==bn+1)
b= bn;
if(a<b){
return a;
} else {
return b;
}
}
void solve(int m){
int k;
cin>>k;
for(int i=1;i<=k;i++){
int ki;
cin>>ki;
arr[i]=ki;
}
for(int j=m;j>=0;j--){
if(j%arr[1]==0){
dp[j]=j/arr[1];
//cout<<dp[j][1];
} else {
dp[j]=bn;
}
}
for(int i=2;i<=k;i++){
for(int j=1;j<=m;j++){
//cout<<dp[j][i-1]<<' ';
if(j-arr[i]>=0){
dp[j]=minval(dp[j-arr[i]]+1,dp[j]);
}
}
//cout<<endl;
}
if(dp[m]==bn){
cout<<"Impossible"<<endl;
} else {
cout<<dp[m]<<endl;
}
}
int main() {
int m;
while(cin>>m){
if(m==0){
return 0;
}
solve(m);
}
return 0;
}
代码写的有点冗余,其实可以更简洁的,顺带一提,动态规划这里其实也可以剪枝哦。