看了不少题解,各种倍增优化,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
时,我们会更新dp
在8,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++;
}
}