货币系统面值构造问题

题目描述: 传统的货币系统是由1,5,10,20,50,100单位的面值组成的,如果要构造一个20单位的面值,可能的方法有:20x1,10x2,10+5X2等等。写一个程序,计算对于给定的货币系统,有多少种方法可以构造出某个单位的面值。

输入: 输入货币系统中的货币种类数V(1<=V<=25),以及所要构造的面值N(1<=N<=1000)。

再输入V个数:货币系统中货币的所有种类的面值。

输出: 所有可能的构造方案数。

样例输入:
3 10
1 2 5
样例输出:
10
下面来对题目进行分析:

​ 首先想到的是穷举,将每一种情况都列举出来,在数量很小的时候是可行的,但是数量大了将会非常低效。

​ 还有什么方法呢?我们一定用过,只是有可能忘记了。

​ 对于这些与生活相关的问题,我们可以想象一下在实际生活中是怎么做的,例如去超市买东西,需要98块钱,如果我不想找零,该怎么付钱呢?当然是凑出这98块了。我们当然可以用98张一块🤭?但是最为实际的方法就是尽可能地用出高面额的货币,在这个例子里面就是尽可能的用出50元。而当我们用出50元时,这个问题就简化为如何用出48元了。下一步当然是尽可能拿出20元了,问题简化为28元。以此类推,这样一个过程就是一个递归的过程,于是很自然的我们想到可以用递归来解决这个问题。

于是我们写下了这样的代码:

//输入过程省略
int typeVal[25];//这里面保存各个面额值
int v;       //货币面额数

long long count(int n)
{//n为面额
	long long sum = 0;//构造方案数
	for (int i = 0; i < v; i++)
	{
		if(n>typeVal[i])//当前剩余金额大于货币面额时,进行简化
			sum += count(n-typeVal[i]);
		if (n == typeVal[i])//当剩余金额正好等于该货币面额时,找到了解决方法
			sum += 1;
	}
	return sum;
}

在这个函数下count(5)将变为

count(5) = count(4)+count(3)+1
而count(4) = count(3)+count(2)
count(3) = count(2)+count(1)
由于count(1) = 1(1)、count(2)=2(11、2)
得到count(5)= count(4)+count(3)+1 = 2*count(3) +count(2)+1 = 9
而实际上5的组成方案数为:4(11111,1112,122,5)

​ 由此可见这个函数是错误的,那么到底是错在哪里了呢?按理说3的组成方案为(111,12)只有两种,而在上例中count(3) = count(1)+count(2)=3种,我们将12和21两种情况都算在里面了,即将排列与组合混淆了。这里的顺序其实是没有任何意义的,那么我们该怎样消除同组合不同顺序的情况呢?

其实在上面的分析中,就已经给出了答案,在生活中我们会倾向于先凑出高面额的货币,来简化过程,而最高货币面额,就是我们在递归中消除重复的利器。

count(3)中,取出2变成count(1),取出1变成count(2),那么我们如何消除先取2后取1和先取1后取2之间的重复呢?

我们可以限制必须以面额从高到低的顺序递归,即如果先取了2,后面可以取2或1,而如果先取了1,后面就只能取小于等于1的面额,不再能够取2了,这样便能够消除重复了。

因此,在每一次递归中,我们可以将当前减去的值作为参数传给递归函数,在下一步递归中,我们最多只能减去这个参数的值。

如此我们便可得到改进的递归函数了:

#include<iostream>
using namespace std;

long long num[10001];   //总次数
int typeVal[50];        //货币各种类面值
//递归方法
long long count(int n, int vmax)
{//n为面额,vmax为最大货币面额的序号
	long long sum = 0;
	for (int i = 0; i < vmax; i++)
	{
		if(n>typeVal[i])
			sum += count(n-typeVal[i], i+1);//这里i+1是因为vmax是序号,而数组是从0开始的
		if (n == typeVal[i])
			sum += 1;
	}
	return sum;
}

int main()
{
	int N;                  //面值
	int V;                  //货币种类数
	cin >> V;
	cin >> N;
	for (int i = 0; i < V; ++i)//输入各面值
		cin >> typeVal[i];
	//递归方法
	cout<<count(N,V)<<endl;
    return 0;
}

由于我们在递归函数中传递的是面额的序号,因此需要将面额数组作为全局变量。

在主函数中调用count函数时,传递N和种类数V作为参数,函数的一遍遍递归将会为我们带来答案。

而从这个递归过程中,我们也能发现其中蕴含的关系,从而得出迭代的实现方式:

//迭代方法-动态规划
long long dp[10][1000];//dp[种类数][总金额]
for (int i = 0; i < N; ++i)
	dp[0][i] = 0;
for (int i = 0; i < V; ++i)
{
	for (int j = 1; j <= N; ++j)
	{
		dp[i][j] = 0;
		for (int m = 0; m <= i; ++m)
		{
			if(j>typeVal[m])
				dp[i][j] += dp[m][j - typeVal[m]];
			if (j == typeVal[m])
				dp[i][j] += 1;
		}
	}
}
posted @ 2020-04-14 13:36  shadowgully  阅读(338)  评论(0编辑  收藏  举报