最少钱币数 DP,暴力搜索解法

  1. 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更新的过程

咱们的状态转移方程就是DP\left [ n \right ]=min\left \{ DP\left [ n-ARR\left [ i \right ] \right ],DP\left [ n \right ] \right \}

这张图的箭头就是影响当前节点的点了。不难看出,这个图的更新方法像不像那个东西,没错就是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;
}



 代码写的有点冗余,其实可以更简洁的,顺带一提,动态规划这里其实也可以剪枝哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值