这主要结合《编程之美-2.18数组分割》来实现,个人觉得《编程之美》中讲得不是特别详细,本人结合新浪微博中“数组分割问题-详细版”的介绍以及自己根据0-1背包问题作出如下实现思想,其中主要思想还是要感谢新浪微博中“数组分割问题-详细版,http://blog.sina.com.cn/s/blog_66223402010164ux.html”的版主的介绍与分析:
《编程之美》2.18解法二中提到,从2n个数中找n个元素,有三种可能:大于Sum/2,小于Sum/2以及等于Sum/2。而大于Sum/2与小于等于Sum/2没区别,故可以只考虑小于等于Sum/2的情况,在此我们仍然沿用这个思想。
首先根据0-1背包问题(http://www.cnblogs.com/fly1988happy/archive/2011/12/13/2285377.html)介绍一下动态动态规划思想 :
起状态转移方程是:f[i][j][k]=max{f[i-1][j][v],f[i-1][j-1][v-a[i]]+a[i]}
其中,F[i-1][j][k]表示前i-1个元素中选取j个使其和不超过但最逼近k;F[i-1][j-1][k-A[i]]在前i-1个元素中选取j-1个元素使其和不超过但最逼近k-A[i],这样再加上A[i]即第i个元素就变成了 选择上第i个元素的情况下最逼近k的和。而第一种情况与第二种情况是完备且互斥的,所以需要将两者最大的值作为F[i][j][k]的值。
伪代码如下:
F[][][]← 0
for i ← 1 to 2*N
nLimit ← min(i,N)
do for j ← 1 to nLimit
do for k ← 1 to Sum/2
F[i][j][k] ← F[i-1][j][k]
if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
return F[2N][N][Sum/2]
当然,前面已经提到,要给出一种方案的打印,下面我们谈谈怎么打印一种方案。
可以设置一个三维数组Path[][][]来记录所选择元素的轨迹。含路径的伪代码如下,只是在上述伪代码中添加了一点代码而已。
F[][][]← 0
Path[][][]← 0
for i ← 1 to 2*N
nLimit ← min(i,N)
do for j ← 1 to nLimit
do for k ← 1 to Sum/2
F[i][j][k] ← F[i-1][j][k]
if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
Path[i][j][k] ← 1
return F[2N][N][Sum/2] and Path[][][]
根据求得的Path[][][]我们可以从F[2N][N][Sum/2]往F[0][0][0]逆着推导来打印轨迹对应的元素。伪代码如下:
i ← 2N
j ← N
k ← Sum/2
while (i > 0 && j > 0 && k > 0)
do if(Path[i][j][k] = 1)
then Print A[i]
j ← j-1
k ← k-A[i]
i ← i-1
上面的伪代码的意思是,每当找到一个Path[][][]=1,就将其对应的A[i]输出,因为已经确定一个所以j应该自减1,而k代表总和,所以也应该减去A[i]。至于为什么不管Path[][][]是否为1都需要i自减1,这一点可以参照本人博文《背包问题——“01背包”详解及实现(包含背包中具体物品的求解)》中的路径求法相关内容。
下面开始优化空间复制度为O(N*Sum/2)
我们观察前面不含路径的伪代码可以看出,F[i][j][k]只与 F[i-1][][]有关,这一点状态方程上也能反映出来。所以我们可以用二维数组来代替三维数组来达到降低空间复杂度的目的。但是怎么代替里面存有玄 机,我们因为F[i][j][k]只与F[i-1][][]有关,所以我们用二维数组来代替的时候应该对F[i][j][k]的“j”维进行逆序遍历。为 什么?因为只有这样才能保证计算F[i][j][k]时利用的F[i-1][j][]和F[i-1][j-1][]是真正i-1这个状态的值,如果正序遍 历,那么当计算F[][j][]时,F[][j-1][]已经变化,那么计算的结果就是错误的。
伪代码如下
F[][]← 0
for i ← 1 to 2*N
nLimit ← min(i,N)
do for j ← nLimit to 1
do for k ← A[i] to Sum/2
if (F[j][k] < F[j-1][k-A[i]]+A[i])
then F[j][k] ← F[j-1][k-A[i]]+A[i]
return F[N][Sum/2] and Path[][][]
上面的伪代码基本上和《编程之美》2.18节最后所给的代码基本一致了,但是里面并不含Path,如果要打印其中一种方案,那么仍需要2N*N*Sum/2的空间来存放轨迹。即
F[][]← 0
Path[][][]← 0
for i ← 1 to 2*N
nLimit ← min(i,N)
do for j ← nLimit to 1
do for k ← A[i] to Sum/2
if (F[j][k] < F[j-1][k-A[i]]+A[i])
then F[j][k] ← F[j-1][k-A[i]]+A[i]
Path[i][j][k] ← 1
return F[N][Sum/2] and Path[][][]
打印路径的伪代码与之前的一模一样,这里不再重写。
下面给出《编程之美》2.18节所讲的“数组分割”中给出的数据进行本文思想的C代码实现:
#include <stdio.h>
#include <math.h>
#define SUM_MAX 100
#define MLEN 20
int min(int a,int b)
{
if (a<b)
return a;
else
return b;
}
void fenge(int a[],int len,int sum)
{
int i,j,k;
int F[MLEN+1][MLEN/2+1][SUM_MAX/2+1]={0};
int nLimit;
int Path[MLEN+1][MLEN/2+1][SUM_MAX/2+1]={0};
for (i=1;i<=len;i++)
{
nLimit=min(i,len/2);
for (j=nLimit;j>=1;j--)
{
for (k=1;k<=sum/2;k++)
{
F[i][j][k]=F[i-1][j][k];
if (k>=a[i-1])
{
if (F[i][j][k]<F[i-1][j-1][k-a[i-1]]+a[i-1])
{
F[i][j][k]=F[i-1][j-1][k-a[i-1]]+a[i-1];
Path[i][j][k]=1;
}
}
}
}
}
i=len;j=len/2;k=sum/2; //打印对应的数组,这里从F[2len][len][sum/2],k表示和
while (i>=0&&j>=0&&k>=0)
{
if (1==Path[i][j][k])
{
printf("%d ",a[i-1]);
--j;
k=k-a[i-1]; //k表示和的形式,所以每次k要减去打印过的数
}
--i;
}
printf("\n");
}
int main()
{
int a[]={1,5,7,8,9,6,3,11,20,17};
int len=sizeof(a)/sizeof(a[0]); //数组长度
int sum=0;
int i;
for (i=0;i<sizeof(a)/sizeof(a[0]);i++)
sum+=a[i];
fenge(a,len,sum);
system("pause");
return 0;
}