1014 Dividing-简单DP,潇洒AC:如何判断一个序列的任意整数之和是否能凑成某个数?

看了不少题解,各种倍增优化,dfs剪枝,写的还都不明不白的,还是自己写一个的比较好。

题目大意

Marsha和Bill拥有一系列大理石。 他们希望将藏品分开,以使两者获得相同的份额。 如果所有的大理石都具有相同的价值,这将很容易,因为那样他们就可以将收藏品分成两半。 但不幸的是,有些大理石比其他大理石更大或更漂亮。 因此,玛莎(Marsha)和比尔(Bill)首先为每个大理石分配一个值,即一个介于1到6之间的自然数。 现在,他们希望对大理石进行分割,以使它们各自获得相同的总价值。 不幸的是,他们意识到以这种方式分割大理石可能是不可能的(即使所有大理石的总价值是均匀的)。 例如,如果存在一个值为1的大理石,值为3的一个,值为4的两个,则不能将它们拆分为相等值的集合。 因此,他们要求您编写一个程序来检查弹珠是否存在合理的分区

思路分析

容易得到要将所有的石头均分,奇数肯定不行,而我们要做的就是判断是否能有一堆石头组成总重量 W W W的一半,这样就可以均分。我们可以比较容易地想到贪心的思想,从大的开始选,大的不行再用小的来补,小的也不行就回溯。但是回溯的代价太高,既然我们已经想到了搜索,而搜索的代价又过高,不如思考一下记忆化搜索?

  • 首先我们肯定需要两个特判,当所有输入元素为0时结束,和为奇数时输出。
int sum = 0;
for (int i = 0; i < 6; i++) { cin >> a[i]; sum += a[i] * (i + 1); }
if (sum == 0) break;
cout << "Collection #" << step << ":" << endl;
if (sum % 2 == 1) {
	cout << "Can't be divided." << endl << endl; step++;  continue;
}
  • 然后我们要判断某个值能否用这个序列中的一些元素拼凑出来,不妨开一个dp[60000](最多20000元素,就算全都是6,一半也就是60000),接下来我们分几步进行分析,首先我们肯定需要一个循环遍历所有的值的个数,当然如果该值的个数为0,或者我们以经找到了结果,就可以continue
//判断能不能由一些元素组合起来值等于 sum/2
int res = sum / 2, maxj = 0; bool dp[60000];//dp[i]:我们是否能凑成i值
for (int i = 0; i < 60000; i++)dp[i] = false;
dp[0] = 1;
for (int i = 0; i < 6; i++) {
	if (!a[i] || dp[res])continue;
}
  • 然后重点就来到了我们要如何组织这个dp,(优化1)我们使用一个值maxj来记录目前以经可以组合的数字的最大值,可以看到对序列2 1 0 1 1 1,再经过第一个点之后maxj=2,在经过第二点后maxj=4,当然这个值不能超过我们的目标值。maxj有什么作用呢?我们可以再加一层循环,从j=maxj开始不断j--,只要dp[j]是我们已经组合出的一个数,那我们就可以在j值的基础上,利用k<=a[i]个元素,不断的更新dp[j+k*i]=1,到此for循环就写成了这样
for (int i = 0; i < 6; i++) {
	if (!a[i] || dp[res])continue;
	for (int j = maxj; j >= 0; j--) {//maxj统计当前出现过的最大值
		if(dp[j]{//j值以经被组合出来了
			for (int k = 1; k <= a[i]; k++) {//遍历所有取的数目
				dp[j + k * (i + 1)] = 1;
			}
		}
	}
	maxj += a[i] * (i + 1);
	if (maxj > res)maxj = res;
}
  • 显然这样的for循环又过于直白,我们的循环次数大概是6*maxj*k,如何对他进行一些优化呢?首先,(优化2)一旦我们找到了dp[res]的值,可以立即break。最后(优化三)在最内层循环中,如果我们的j + k * (i + 1)>res,那我们就没有继续下去的必要了,可以break判断下一个j值。
  • 还有最后一个稍微难以理解一点的优化,看示例:对于序列4 0 0 3 0 0,当我们判断到4的时候,maxj应该是4,当j=4时,我们会更新dp8,12,16三个位置的值。再看j=0的时候,我们会更新dp[4]的值你会发现,dp[4]我们用了一个1,接下来我们势必会更新8,12,这些更新有必要吗?没有!所以每次在最内层循环中,我们发现某个值dp[j+k*(i+1)]是一个已经组合出的值时,就可以break了,无需再继续下去。(可以将300ms的程序优化到3ms)
    在这里插入图片描述
#include<iostream>
#include<algorithm>
using namespace std;

int a[6];

int main() {
	int step = 1;
	while (step) {
		int sum = 0;
		for (int i = 0; i < 6; i++) { cin >> a[i]; sum += a[i] * (i + 1); }
		if (sum == 0) break;
		cout << "Collection #" << step << ":" << endl;
		if (sum % 2 == 1) {
			cout << "Can't be divided." << endl << endl; step++;  continue;
		}
		//判断能不能由一些元素组合起来值等于 sum/2
		int res = sum / 2, maxj = 0; bool dp[60000];//dp[i]:我们是否能凑成i值
		for (int i = 0; i < 60000; i++)dp[i] = false;
		dp[0] = 1;
		for (int i = 0; i < 6; i++) {
			if (!a[i] || dp[res])continue;
			for (int j = maxj; j >= 0; j--) {//maxj统计当前出现过的最大值
				if (dp[j] && !dp[res]) {//j值以经出现过了,但目标值没有出现过
					for (int k = 1; k <= a[i]; k++) {//遍历所有取的数目
						if (j + k * (i + 1) == res) {//恰好凑成了目标值
							dp[res] = 1; break;
						}
						if (j + k * (i + 1) > res || dp[j + k * (i + 1)])break;//已经超过了目标值
						dp[j + k * (i + 1)] = 1;
					}
				}
			}
			maxj += a[i] * (i + 1);
			if (maxj > res)maxj = res;
		}
		if (dp[res])cout << "Can be divided." << endl << endl;
		else cout << "Can't be divided." << endl << endl;
		step++;
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值