参照了毛子青的《动态规划算法的优化》
[问题描述]
有价值分别为1..6的大理石各a[1..6]块,现要将它们分成两部分,使得两部分价值和相等,问是否可以实现。其中大理石的总数不超过20000。
令S=∑(i*a[i]),若S为奇数,则不可能实现,否则令Mid=S/2,则问题转化为能否从给定的大理石中选取部分大理石,使其价值和为Mid。
这实际上是母函数问题,用动态规划求解也是等价的。
m[i, j],0≤i≤6,0≤j≤Mid,表示能否从价值为1..i的大理石中选出部分大理石,使其价值和为j,若能,则用true表示,否则用false表示。则状态转移方程为:
m[i, j]=m[i, j] OR m[i-1,j-i*k] (0≤k≤a[i])
规划的边界条件为:m[i,0]=true; 0≤i≤6
若m[i, Mid]=true,0≤i≤6
但这样做超时
实现代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
int const maxn = 30010;
bool dp[7][maxn];
int value[7];
int main()
{
int num = 1;
while(true)
{
memset(dp, 0, sizeof(dp));
int val = 0;
for(int i = 1; i <= 6; i++)
{
scanf("%d", &value[i]);
val += i * value[i];
}
if(val == 0)
break;
printf("Collection #%d:\n", num++);
if(val % 2 == 1)
{
printf("Can't be divided.\n\n");
continue;
}
int mid = val / 2;
for(int i = 0; i <= 6; i++)
dp[i][0] = true;
for(int i = 1; i <= 6; i++)
{
for(int j = 0; j <= mid; j++)
{
for(int k = 0; k <= value[i]; k++)
{
if(j >= k * i)
dp[i][j] |= dp[i-1][j - k * i];
}
}
}
if(dp[6][mid])
printf("Can be divided.\n\n");
else
printf("Can't be divided.\n\n");
}
return 0;
}
按照他说的双向动态规划~
状态转移方程改进为:
当i≤3时:
m[i, j]=m[i, j] OR m[i-1,j-i*k] (1≤k≤a[i])
当i>3时:
m[i, j]=m[i, j] OR m[i+1,j-i*k] (1≤k≤a[i])
规划的边界条件为:m[i,0]=true; 0≤i≤7
这样,若存在k,使得m[3,k]=true, m[4,Mid-k]=true
我不知道自己理解错误还是,代码实现错误,还是超时~~(有谁用这种方法实现过了的??)
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int const maxn = 30010;
int value[7];
bool dp[7][maxn];
int main()
{
int num = 1;
while(true)
{
memset(dp, 0, sizeof(dp));
int val = 0;
for(int i = 1; i <= 6; i++)
{
scanf("%d", &value[i]);
val += i * value[i];
}
if(val == 0)
break;
printf("Collection #%d:\n", num++);
if(val % 2 == 1)
{
printf("Can't be divided.\n\n");
continue;
}
int mid = val / 2;
for(int i = 0; i <= 7; i++)
dp[i][0] = true;
for(int i1 = 1, i2 = 6; i1 <= 3 || i2 > 3; i1++, i2--)
{
for(int j = 0; j <= mid; j++)
{
for(int k = 0; k <= value[i1] || k <= value[i2]; k++)
{
if(k <= value[i1])
{
if(j >= k * i1)
dp[i1][j] |= dp[i1-1][j - k * i1];
}
if(k <= value[i2])
{
if(j >= k * i2)
dp[i2][j] |= dp[i2+1][j - k * i2];
}
}
}
}
bool flag = false;
for(int k = 0; k <= mid; k++)
{
if(dp[3][k] && dp[4][mid-k])
{
flag = true;
printf("Can be divided.\n\n");
break;
}
}
if(!flag)
printf("Can't be divided.\n\n");
}
return 0;
}
上面两种方法都超时,后面看了http://blog.csdn.net/zxy_snow/article/details/6169008这位神牛的,是用二进制拆分实现的,直接抄过来吧。。。。。
这个问题其实就是0/1背包问题,只是维度不是之前的1维了,因为每种分数的大理石不止一个,是多个。
所以我们可以转成一维的,把每种分数多少个大理石从小到大摊开来,比如所1分的大理石有两个,2分的大理石有3个,那么从小到大排列就是1*1, 1*1, 2 *1, 2 * 2, 被乘数表示分数,乘数表示个数,然后就可以用0/1背包来解决了
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int const maxn = 300010;
int value[7], num[maxn], tot;
int dp[maxn];
void split(int n, int v);
int main()
{
int n= 1;
while(true)
{
memset(dp, 0, sizeof(dp));
int val = 0;
for(int i = 1; i <= 6; i++)
{
scanf("%d", &value[i]);
val += i * value[i];
}
if(val == 0)
break;
printf("Collection #%d:\n", n++);
if(val % 2 == 1)
{
printf("Can't be divided.\n\n");
continue;
}
int mid = val / 2;
tot = 0;
for(int i = 1; i <= 6; i++)
split(value[i], i);
for(int i = 0; i < tot; i++)
{
for(int j = mid; j >= num[i]; j--)
{
if(dp[j - num[i]] + num[i] > dp[j])
dp[j] = dp[j - num[i]] + num[i];
}
}
if(dp[mid] == mid)
printf("Can be divided.\n\n");
else
printf("Can't be divided.\n\n");
}
return 0;
}
void split(int n, int v)
{
int i = 0, x, tmp = 0;
while(true)
{
x = 1 << i;
if(tmp + x > n)
break;
tmp += x;
num[tot++] = x * v;
i++;
}
x = n - tmp;
if(x != 0)
num[tot++] = v * x;
}