首页 > 代码库 > 硬币问题——固定终点的最长路和最短路

硬币问题——固定终点的最长路和最短路

问题描述:

       有n种硬币,面值分别为V1,V2...,Vn,每种都有无限多。给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S?输出硬币数目的最小值和最大值。0 <= n <= 100, 0 <= S <= 10000, 1 <= Vi <= S。


分析:
       本题的本质还是DAG上的路径问题。我们把每种面值看作一个点,表示"还需要凑足的面值",则初始状态为S,目标状态为0。若当前的状态i,每使用一个硬币j,状态便转移到i-Vj。这个模型和嵌套矩形一题类似,但也有些明显的不同之处:嵌套矩形中并没有确定路径的起点和终点(可以把任意矩形放在第一个和最后一个),而本题的起点必须是S,终点必须是0。把终点固定之后"最短路"才是有意义的。在嵌套矩形中,最短序列显然是空(如果不允许空的话,就是单个矩形,不管怎样都是平凡的),而本题的最短路径却不是那么容易确定的。


       接下来考虑"硬币问题"。注意到最长路和最短路的求法是类似的,下面只考虑最长路。由于终点固定,d(i)的确切含义变为"从节点i出发到节点0的最长路径长度"。

下面是求最长路的代码(错误的):

int dp(int S)//错误 
{
	int& ans=d[S];
	if(ans>=0) return ans;
	ans=0;
	for(int i=1;i<=n;i++)if(S>=V[i])
		ans=max(ans,dp(S-V[i])+1);
		
	return ans; 
}
/*此代码有一个致命的错误,即由于节点S不一定真的能到达节点0,所以需要用特殊的d[S]值表
示"无法到达",但在上述代码中,如果S根本无法继续往前走,返回值是0,将被误以为是"不能
走已经到达终点"的意思。*/



正确的解法一:

int dp(int S)	
{
	int& ans=d[S];
	if(ans != -1) return ans;
	ans=-1<<30;
	for(int i=1;i<=n;i++)if(S>=V[i])
		ans=max(ans,dp(s-V[i])+1);
		
	return ans; 
}
注意:在记忆化搜索中,如果用特殊值表示"还没有算过",则必须将其和其他特殊值(如无解)区分开。


正确的解法二:

int dp(int S)
{
	if(vis[S]) return d[S];
	vis[S]=1;
	int& ans=d[S];
	ans=-1<<30;
	for(int i=1;i<=n;i++)if(S>=V[i]){
		ans=max(ans,dp(S-V[i])+1);
	} 	
	return ans;
}
       尽管多了一个数组,但可读性增强了许多:再也不用担心特殊值之间的冲突了,在任何情况下,记忆化搜索的初始化都可以用memset(vis,0,sizeof(vis))执行。



      本题要求最小、最大两个值,记忆化搜索就必须写两个。在这种情况下,还是递推来得更加方便(此时需注意递推的顺序):

min[0]=max[0]=0;
for(int i=1;i<=n;i++)
{
    min[i]=INF; max[i]=-INF;
}
for(int i=1;i<n;i++)
   for(int j=1;j<=v;j++)
         if(i>=V[j])
      {
          min[i]=min(min[i],min[i-V[j]]+1);
          max[i]=max(max[i],max[i-V[j]]+1);
         }
printf("%d %d\n",min[S],max[S]);


完整代码:

#include "stdio.h"
#define INF 1<<30
#define maxn 100+10
int V[maxn],n;
int min[maxn],max[maxn];

inline int Min(int a,int b){return a<b?a:b;}
inline int Max(int a,int b){return a>b?a:b;}

//打印可行的方案 
void print_ans(int* d,int S){
	for(int i=1;i<=n;i++){
		if(S>=V[i] && d[S]==d[S-V[i]]+1){
			printf("%d ",V[i]);
			print_ans(d,S-V[i]);
			break;
		}
	}
}

int main()
{
	int S;
	//输入面值S和面值的种数n 
	while(~scanf("%d%d",&S,&n))
	{		
		for(int i=1;i<=n;i++){
			scanf("%d",&V[i]);
		}
		
		max[0]=0; min[0]=0;
		for(int i=1;i<=S;i++)
		{
			min[i]=INF; max[i]=-INF;
		}
		
		//递推实现 
		for(int i=1;i<=S;i++){
			for(int j=1;j<=n;j++){
				if(i>=V[j]){
					min[i]=Min(min[i],min[i-V[j]]+1);
					max[i]=Max(max[i],max[i-V[j]]+1);
				}
			}
		}
		
		print_ans(min,S);	printf("    min\n");
		print_ans(max,S);	printf("    max\n");
		printf("min:%d max:%d\n",min[S],max[S]);	
	}
	
	return 0;
}