题干
有一个无序,元素个数为2N的正整数数组。要求:如何这个数组分割为元素个数为N的两个数组,并使两个子数组的和最接近。
我的解法
我的解法居然没有出现在书上和博客上,所以很让我怀疑我的解法有漏洞,但是又死活找不到。而且我的解法也可以应对原数组中有负数的情况。
如下图所示,将数组排序后:
- 最小的1和最大的20,给上面的一个子数组
- 次小的3和次大的17,给下面的数组
- 然后一直重复
- 剩下两个7和8的时候,一个给上面一个给下面
另外我还写了代码完成这个过程:
/**
* 编程之美 2.18题
* zy的解法,虽然结果正确,但是书上没有这样的解法,很不踏实,但是又找不出错
*/
#include <stdio.h>
#include <stdlib.h>
#define min(x,y) x<y?x:y
/**
* 测试用例
*/
int a1[]={1,5,7,8,9,6,3,11,20,17};
int a2[]={1,199,3,200};
int a3[]={0,0};
int a4[]={-5,-4,-3,-2,-1,1,2,3,4,5};
int *sub1;
int *sub2;
intcomp(const void *a,const void *b)
{
return*(int*)a-*(int*)b;
}
void get2subArray(int *a,int length){
int h;
int *i,*j;
int *p=a;
qsort(a,length,sizeof(int),intcomp);
printf("原数组:");
for(h=0;h<length;++h)
printf("%2d ",*p++);
int subLength=length/2;//这我们要求的子数组的长度,题设要求为原数组的一半
sub1=(int *)malloc((subLength)*sizeof(int));
sub2=(int *)malloc((subLength)*sizeof(int));
int *q1=sub1;
int *q2=sub2;
i=a;
j=a+length-1;
int flag=0; //标志:表明这一次数组的最大数和最小数给一个数组,下一轮给另一个数组
for( ; i<j; ++i,--j){//i<j不能去掉
if(*(i+1)==*j && (length/2)%2==1 ){//后一个条件:原数组长度的一半为奇数,则最后一个子数组拿走原数组的一个元素
*q1=*i;
*q2=*j;
break;
}
if(flag){//这一次给sub1
*q1++=*i;
*q1++=*j;
flag=0;
}else{//这一次给sub2
*q2++=*i;
*q2++=*j;
flag=1;
}
}
printf("\n数组1:");
p=sub1;
for(h=0;h<subLength;h++)
printf("%2d ",*p++);
printf("\n数组2:");
p=sub2;
for(h=0;h<subLength;h++)
printf("%2d ",*p++);
printf("\n");
}
int main() {
int length=sizeof(a1)/sizeof(a1[0]);
get2subArray(a1,length);
printf("\n");
length=sizeof(a2)/sizeof(a2[0]);
get2subArray(a2,length);
printf("\n");
length=sizeof(a3)/sizeof(a3[0]);
get2subArray(a3,length);
printf("\n");
length=sizeof(a4)/sizeof(a4[0]);
get2subArray(a4,length);
}
运行结果:
asd@asd-desktop:~/workspace/test/src$ ./a.out
原数组: 1 3 5 6 7 8 9 11 17 20
数组1: 3 17 6 9 7
数组2: 1 20 5 11 8
原数组: 1 3 199 200
数组1: 3 199
数组2: 1 200
原数组: 0 0
数组1: 0
数组2: 0
原数组:-5 -4 -3 -2 -1 1 2 3 4 5
数组1:-4 4 -2 2 -1
数组2:-5 5 -3 3 1
asd@asd-desktop:~/workspace/test/src$
书上的解法
动态规划
书上说着这题涉及动态规划,加之我对动态规划又不太了解,所以我又把动态规划的思路学习了一遍,很可惜,我仍然觉得学的并不透彻。主要的参考有:
- 算法导论(第三版)第十五章 的刚条切割和最长公共子序列
- 麻省理工学院公开课:算法导论 动态规划,最长公共子序列
- 01背包问题 :该题和01背包问题极其相似
- 程序员面试100题之十五:数组分割
实际上上面第四个参考的博客已经将这道题讲的非常透彻了,但是我对本题还有一点小小的想法。
一个动态规划的算法设计如下,摘自算法导论 P204
- 刻画一个最优解的结构特征
- 递归地定义最有解的值
- 计算最有解的值,通常采用自底向上的方法
- 利用计算出的信息构造一个最有解
另外,动态规划的时间复杂度我发现都是2^N,如果采用了带备忘录的自顶向下的方法和自底向上法,那么时间复杂度将会是N^2。本题也不例外。分析的话,自己看书或者公开课都可以了解。
未优化解法思路
书上说对于优化前的方法说的不算特别清楚,首先,我们要明白我们的核心,那就是找到两个个子数组,个数都为N,其和最接近SUM / 2,SUM就是给定的原数组 ,个数2N的所有元素的和。如果两个子数组都等于SUM / 2,那当然是极好的,不过也有可能,一个子数组的和大于SUM/2,另一个小于SUM/2,这样的情况应该更为常见。
所以根据书上给出的动态规划算法步骤,可以这样完成。
第一步:
- 刻画一个最优解的结构特征,用以下方式完成。
- 用S(k, i)表示前k个元素中任意i个元素的和的集合(写法 摘自博客:程序员面试100题之十五:数组分割 )
- 比如:S(k, 1) = {A[i] | 1<= i <= k}
- 比如:S(k, k) = {A[1]+A[2]+…+A[k]}
动态规划的第二步:
- 递归地定义最有解的值
- S(k, i) = S(k-1, i) U {A[k] + x | x属于S(k-1, i-1) }(写法 摘自博客:程序员面试100题之十五:数组分割)
动态规划的第三步:
- 计算最有解的值
- 那么我认为就是利用上面的公式写代码。
用了这三步,我们就可以得到S(2N, N),在这里找出与SUM/2最接近的那个和,并可得到其中所求两个数组的其中一个数组的和。
动态规划的第四步:
- 利用计算出的信息构造一个最有解
- 也就是拿到具体的数组
- 这一步实际也在写代码的时候考虑的。在刚条切割和最长公共子序列都使用了额外的数组,所以我想这里也必然是需要额外的数组的。
优化后的解法思路
优化的方法就是:带备忘录的自顶向下的方法和自底向上法。
这里是自底向上法,这主要是因为动态规划中会计算重复计算大量的相同的子问题,如果我们从小问题计算到大问题,那么小问题已经被事先计算了,所以就不会在计算大问题的时候重复计算小问题了
注意:
- 第34行v的取值。
- 第35行:isOk[i-1][v-a[k]] 这表示比isOk[i][v]更小规模的问题已经解决了之后,再解决的更大规模的问题。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#define min(x,y) x<y?x:y
int main() {
int a[]={1,5,7,8,9,6,3,11,20,17};
int length=sizeof(a)/sizeof(a[0]);
int subLength=length/2;//这我们要求的子数组的长度,题设要求为原数组的一半
int sum=0;
int i,k,v; //循环用
for(i=0;i<length;i++){
sum+=a[i];
}
printf("sum:%d\n",sum);
/**
* isOk[i][v]的含义是:表示是否找到i个数,使得他们的之和等于v
* 因为我们关心个数为subLength的数组和sum/2的和
* 所以这样初始化数组,数组从0开始,所以必须要+1
*/
int **isOk;
/*动态分配二维数组*/
isOk=(int **)malloc((subLength+1)*sizeof(int *));
for(i=0;i<sum/2+1;i++)
isOk[i]=(int *)malloc(sizeof(*isOk));
isOk[0][0]=1; //不这样数组跑不起来,没有具体含义
/*动态规划的核心所在*/
for(k=0;k<length;++k){ //这里为了考察原数组的每一个元素
for(i = min(k,subLength) ; i >= 1 ; --i){//min存在的含义就是我们要的个数最大只有subLength
for(v=1;v<=sum/2;v++){//同样,v也只会最多到sum/2
if(v>=a[k] && isOk[i-1][v-a[k]]){//第二个判断是看能不能将这个a[k]加入到新组合中
isOk[i][v]=1;
}
}
}
}
for(v = sum/2+1 ; v >= 0 ; --v)
{
if(isOk[subLength][v]){
printf("找到最接近sum/2的数:%d\n",v);
break;
}
}
}
运行结果:
asd@asd-desktop:~/workspace/test/src$ ./a.out
sum:87
找到最接近sum/2的数:43
asd@asd-desktop:~/workspace/test/src$
进一步思考
书上的答案和网上博客的解答只是得到了离sum/2最近的那个值,也就是子数组的和的大小。那么子数组该如何得到呢?