【POJ】剑指DP:1742.Coins

0.概述

楼天城男人八题之一,据说是最简单的一题。
有若干种面值的硬币,每种面值的数量都是有限的。问用这一堆硬币,一共能拼凑出多少种总价值(最大到m)?

1.分析

背包问题,一共能拼成多少种总价值,那就从1开始一直数到m,设当前为w。背包容量为w,问题转为能否把背包恰好装满。
是多重背包问题。
多重背包找可行性,那么是有套路的。写dp表,dp[i][j]的含义是用前i种硬币,组成总数为j,第i种硬币还剩多少,有一说一,这么绕,让我自己想肯定想不出。
那么对于dp[i+1][j],其含义是:用前i+1种硬币,组成总数为j,第i+1种硬币还剩多少。
那么我们想,如果用前i种硬币,就能组成总数为j,那么第i+1种硬币就可以一个不用。怎么才能知道“用前i种硬币,能组成总数为j”呢?只需要dp[i][j]>=0即可,这就表示组成总数为j,第i种硬币还有剩,或者恰好用完。同时我们约定dp[i][j]<0表明不能组成,即可。
如果真就dp[i][j]<0,那怎么办?那就说明我们需要用到第i+1种硬币了。
现在又有两种情况,如果总数j比第i+1种面值小,那么第i+1种硬币用不上,那么就说明没法拼成,dp[i+1][j]=-1;如果总数j比第i+1种硬币的面值大,或者刚好相等,那么我们怎么办?
那肯定看总数j减去面值i+1之后,剩下的能不能用前i+1种表示啊。那这个怎么看?这个是dp[i+1][j-(i+1)]。等下,既然我们用至少一个i+1,那么剩下的最多用i应该就行了!也就是说,看dp[i][j-(i+1)],那我们还得看总数j减去两个面值i+1的硬币之后行不行,也就是dp[i][j-(i+1)*2],还有dp[i][j-(i+1)*3],这么多,得看到什么时候啊?我们来看,dp[i][j-(i+1)*2],就是dp[i][j-(i+1)]去掉一个i+1,如果dp[i][j-(i+1)]>0,那么一定有dp[i][j-(i+1)*2]>0,由此得证。
这就是具体思路。
其实思路并不难,重头戏在后面:卡常数,没优化好就会TLE。来看我TLE的代码:

#include<stdio.h>
#include<cstring>
using namespace std;
//vector<vector<int> > dp(2010, vector<int>(2010, 0));用这个会爆内存
int dp[2][100010];
int HT[101];
int v[101];
int main()
{
	int n, m;
	//cin >> n >> m;我以为cin比scanf效率低,所以用scanf代替了cin
	scanf("%d%d", &n, &m);
	while (n != 0 || m != 0)
	{
		memset(dp, -1, sizeof(dp));
		memset(v, 0, sizeof(v));
		memset(HT, 0, sizeof(HT));
		for (int i = 1; i <= n; ++i)
		{
			//int a;
			//scanf("%d", &a);
			//v[i]=a;
			scanf("%d", &v[i]);
		}
		long TM = 0;
		for (int i = 0; i < n; ++i)
		{
			//int a;
			//scanf("%d", &a);
			//HT[v[i+1]]=a;这句是TLE的元凶
			scanf("%d", &HT[v[i + 1]]);
			TM += v[i + 1] * HT[v[i + 1]];
		}
		//dp[i][j]表示用前i种数堆到和为j,第i种最多还剩多少
		m = TM > m ? m : TM;
		for (int i = 1; i <= n; ++i)
		{
			int flag = i % 2;
			dp[flag][0] = HT[v[i]];
			for (int j = 1; j <= m; ++j)
			{
				if (dp[1-flag][j] >= 0)
					dp[flag][j] = HT[v[i]];//这句是TLE的直接原因
				//else if (j < v[i] || dp[flag][j - v[i]] <= 0)
					//dp[flag][j] = -1;
				else if(j - v[i]>=0 && dp[flag][j - v[i]] >0)
					dp[flag][j] = dp[flag][j - v[i]] - 1;
			}
		}
		int res=0;
		for (int i = 1; i <= m; ++i)
			if (dp[n%2][i] >= 0)
				++res;
		//cout << res << endl;
		//cin >> n >> m;
		printf("%d\n", res);
		scanf("%d%d", &n, &m);
	}
	return 0;
}

TLE本质就是循环体没写好,卡循环体的常数了。那我是怎么没写好呢?观察可知,我为了得到第i个面值的数量,选择了用一个数组去保存面值,然后用另外一个数组保存数量,第二个数组的下标是第一个数组元素,即面值,这样,当我执行 HT[v[i]]时,要访问两次数组,这就会导致TLE。优化之后访问一次就不会了。

2.总结

实话实说,如果练过多重背包的题,这道题还是不难的,但是卡常数的确让人郁闷。
ps:代码如下:

#include<stdio.h>
#include<cstring>
using namespace std;
//vector<vector<int> > dp(2010, vector<int>(2010, 0));
//int dp[2][100010];
int dp[100010];
int HT[101];
int v[101];
int main()
{
	int n, m;
	//cin >> n >> m;
	scanf("%d%d", &n, &m);
	while (n != 0 || m != 0)
	{
		memset(dp, -1, sizeof(dp));
		memset(v, 0, sizeof(v));
		memset(HT, 0, sizeof(HT));
		for (int i = 1; i <= n; ++i)
		{
			//int a;
			//scanf("%d", &a);
			//v[i]=a;
			scanf("%d", &v[i]);
		}
		long TM = 0;
		/*for (int i = 0; i < n; ++i)
		{
			//int a;
			//scanf("%d", &a);
			//HT[v[i+1]]=a;
			scanf("%d", &HT[v[i + 1]]);
			TM += v[i + 1] * HT[v[i + 1]];
		}*/
		for (int i = 1; i <= n; ++i)
		{
			//int a;
			//scanf("%d", &a);
			//HT[v[i+1]]=a;
			scanf("%d", &HT[i]);
			TM += v[i] * HT[i];
		}
		//dp[i][j]表示用前i种数堆到和为j,第i种最多还剩多少
		m = TM > m ? m : TM;
		for (int i = 1; i <= n; ++i)
		{
			//int flag = i % 2;
			//dp[flag][0] = HT[v[i]];
			dp[0] = HT[i];
			for (int j = 1; j <= m; ++j)
			{
				//if (dp[1-flag][j] >= 0)
					//dp[flag][j] = HT[v[i]];
				//else if (j < v[i] || dp[flag][j - v[i]] <= 0)
					//dp[flag][j] = -1;
				//else if(j - v[i]>=0 && dp[flag][j - v[i]] >0)
					//dp[flag][j] = dp[flag][j - v[i]] - 1;
				if (dp[j] >= 0)
					dp[j] = HT[i];
				else if (j < v[i] || dp[j - v[i]] <= 0)
					dp[j] = -1;
				else
					dp[j] = dp[j - v[i]] - 1;
			}
		}
		int res=0;
		for (int i = 1; i <= m; ++i)
			if (dp[i] >= 0)
				++res;
		//cout << res << endl;
		//cin >> n >> m;
		printf("%d\n", res);
		scanf("%d%d", &n, &m);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值