PAT 1068 Find More Coins C++版

1.题意

给出拥有的硬币数和待支付的金额,以及每枚硬币的价值。
现在欲让你求出需要什么样的硬币才能精确的支付金额。

2.分析

解答这题的方法有两种:

2.1 深搜

深搜的思想就是“放/不放” 两种选择,然后匹配出精确的金额即可。但是需要说明的是:如果这题使用深搜实现,好像最后一个测试用例会超时(最后一个测试用例只有1分)。

2.2 动态规划

暂未实现。

3.代码

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<functional>

#define maxn 10005
using namespace std;

int N , M;
int coins[maxn];
int rec[maxn]; 
int COUNT = 0;//解的个数 
int res[maxn];//存下所有解 
int num = 0;
int flag = 0;//表示是否存在解 

//使用深搜算法解决这个和问题 
//对于每枚硬币,都有放或者不放这两种选择,在dfs代码中实现这两种选择即可 
void dfs(int root,int index,int sum){//root表示是0 			
	if( sum > M){//如果超过了M 
		return ;
	} 
	else if(sum == M){		
		if(COUNT == 0){	
			num = index;		
			for(int i = 0;i< index;i++){
				res[i] = rec[i];//保存到res中 
			}
		}
		COUNT ++;//解数+1 
		flag = 1;
	} 	
	if(flag == 1) return ; 
	
	//继续往下递归 	=> 放硬币 	
	rec[index] = root;
	
	dfs(root+1,index+1,sum + coins[root]);
	rec[index] = 0;//因为上面已经修改了值,所以这里要重置为0 
		
	//继续往下递归 	=> 不放硬币 			
	if(root + 1 < N) dfs(root+1,index,sum);	
}

int main(){	
	cin >> N >> M;
	int i,j;
	
	for(i = 0;i< N;i++){
		cin >> coins[i];
	}
	sort(coins,coins+N,less<int>());	
	
	int index = 0;//下标 
	int sum = 0;//总和 
	for(i = 0;i< N;i++){
		index = 0;//重置为0 
		sum = 0;//重置为0 
		dfs(i,index,sum);//从i开始 			
	} 
	
	if(COUNT == 0 ){
		cout << "No Solution\n";
		return 0; 	
	}
	for(j = 0;j < num;j++){
		if(j!=num-1) cout << coins[res[j]]<<" " ;
		if(j == num-1) cout << coins[res[j]] ;
	}	
}

4.测试用例

给出如下几组测试用例:

8 9
5 9 8 7 2 3 4 1

4 8
7 2 4 3

4 8
7 2 4 4

4 8
7 1 4 4

5.坑点

  • 使用递归会发生超时的问题—— 最后一个测试用例过不去
    PAT 1068 Find More Coins C++版_c
    可能是我用的剪枝优化不够优化,所以仍然没有什么明显的效果。【不是剪枝问题  ̄へ ̄】
== update on 2019 05 18 ==
近些天在学习DP,在刷PAT的时候,看到这道题,就想当做DP练个手。在用DP之前,透露一点儿内容就是:即使这道题不用DP,用深搜也是可以解决的。用深搜可以解决这道题的主要一个原因是**“姥姥设计的测试用例不够好”**,对于上述没有过的测试用例如果我们在dfs之前调用如下的一个if判断即可:
if(temp < M){
		cout <<"No Solution\n";
		return;
	} 

这里的 temp 是整个序列的所有和。当然,这只是一个小伎俩,没有什么鸟用,对于这题还是应该用dp。

===========================分割线============================
对于这题应该很多人都会直觉上使用dfs,毕竟dp是什么鬼玩意儿。。。
看了别人写的代码之后,才知道这题是考dp,而且是考01背包那种dp。网上很多代码,相信如果没有接触过dp的人,大多数是看不懂的,在看解题思路之前,先做一下如下介绍:

  • w[maxn]表示的是每个硬币的价值
  • dp[maxn][maxn],其中dp[i][j]表示的是在添加前i项(包括第i项)物品,容量为j的情况下所能达到的最大价值
  • choice[maxn][maxm]choice[i][j]=1表示的是第i项作为结果序列加入,choice[i][j]=0表示的第i项未加入。

对于动态规划可以解决的题的特征我就不再啰嗦了,可以详细见我的专题【updating】讲解。
我们视这个货币为重i且价值为i的货币,这样就可以把这个计算货币的问题,转化为背包问题了。【可能又有人问:背包里计算的是最大价值,这里计算的是固定价值,怎么行呢?我们这么想:背包计算的是体积v中最大的价值m;如果对于v/m=1的物品来说,只有装满,才能得到最大的价值v(=m),同理,我们这里只需要找出体积v时所能装到的最大的值v,并且判断这个v是不是输入中的总价值即可。于是问题就转化为 => 求在体积v下的最大价值是否是v,如果是,则输出最优解序列】

  • step1:递归判断过程(其实动态规划就是这个递归判断式子是最重要的,其余的都是扯淡)无非如下两种:
    (1)如果dp[i-1][j](不放第i件物品的情况) >= dp[i-1][j-w[i]] + w[i](放第i件物品的情况),则dp[i][j] = dp[i-1][j-w[i]] + w[i] (因为要放第i件物品,所以要为第i件物品腾出体积,当前的体积是j)
    (2)如果dp[i-1][j](不放第i件物品的情况) < dp[i-1][j-w[i]] + w[i](放第i件物品的情况),则dp[i][j] = dp[i][j]

  • step2:在第一步更新的时候,如果有更新dp[i][j],则将相应的choice[i][j]重置为1,否则置为0。

  • step3:判断dp[n][m]是否和m相等,如果不相等,则直接输出No Solution,否则执行第四步

  • step4:根据choice[][]数组,找出具体的最优解。寻找过程如下:
    对于choice[k][v](初始值:k=n,v=m),判断choice[i][j]是否是1,如果是1,表示的是加入了第i件物品,则将v-=w[i](因为要回到体积为j-w[i]的地步),同时进行k–处理(因为当前的第i件物品处理完了,需要判断第k-1物品)。将整个choice[k][v]==1的k保存下来就是我们需要的解(需要始终记住:k指的是第k件物品放没放;v指的是该时的体积有多少。

针对上述四个步骤,得到的代码如下:

#include<iostream> 
#include<cstdio>
#include<algorithm>
#define maxn 10005
#define maxm 105

using namespace std;

int N,M;
int w[maxn];//总重量 == 总价值 
int dp[maxn][maxm];//用于存储解的动态数组 
int choice[maxn][maxm];//表示选择了哪个 
int flag[maxn];//标记输出 
int index = 0;

int cmp(int a,int b){
	return a > b;
}

void solve(){
	//对数组进行初始化操作 	
	int i,j;
	for(i = 1;i <=N ;i++ ){//表示当前的 
		for(j = 1;j <= M ;j++){//j表示 当前的容量
			if(j < w[i]){//如果当前的容量小于w[i] 
				dp[i][j] = dp[i-1][j];
				choice[i][j] = 0; 
			}else{
				//如果放的时候大于等于不放的时候 ,则应该使用放的情况 
				if( (dp[i-1][j-w[i]] + w[i] ) >= dp[i-1][j]){
					dp[i][j] = 	dp[i-1][j-w[i]] + w[i] ;//取两者较大值 
					choice[i][j] = 1; //标记i放进去了 
				}
				else if((dp[i-1][j-w[i]] + w[i] ) < dp[i-1][j]) {
					dp[i][j] =  dp[i-1][j];						
					choice[i][j] = 0; 
				}				
			}
		}
	}
} 

//找出解的过程 
void find(){
	int k = N,v= M;	
	while(k >= 0){
		if(choice[k][v] == 1){//说明当前节点加入到了序列中 
			flag[k] = 1;
			v -= w[k];
			index++;
		}
		else{
			flag[k] = 0; 			
		}
		k--;
	}
} 

int main(){
	cin >> N >> M;
	int i ,j;
	fill(dp[0],dp[0]+maxn*maxm,0);
	
	//输入数字  下标从1开始 
	for(i = 1;i <= N ;i++){
		cin >> w[i];		
	}	
	sort(w+1,w+N+1,cmp); 	
	solve(); 
	find();	
	if(dp[N][M]!=M) cout <<"No Solution\n";
	else{
		for(i = N;i >=1; i-- ) {
			if(flag[i] == 1) {
				if( index > 1) 			cout << w[i] <<" ";	
				else cout << w[i];
				index--;
			}		
		}	
	}		
}

如下给出测试用例

4 5
1 2 3 4

所对应的执行步骤【跟上述的程序不是同一个程序。这里只是做一下手动调试而已】
PAT 1068 Find More Coins C++版_# PAT_02