编程之美 2.18:数组分割 (涉及 动态规划)

题干

有一个无序,元素个数为2N的正整数数组。要求:如何这个数组分割为元素个数为N的两个数组,并使两个子数组的和最接近。

我的解法

我的解法居然没有出现在书上和博客上,所以很让我怀疑我的解法有漏洞,但是又死活找不到。而且我的解法也可以应对原数组中有负数的情况。
如下图所示,将数组排序后:
《编程之美 2.18:数组分割 (涉及 动态规划)》

  1. 最小的1和最大的20,给上面的一个子数组
  2. 次小的3和次大的17,给下面的数组
  3. 然后一直重复
  4. 剩下两个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$ 

书上的解法

动态规划

书上说着这题涉及动态规划,加之我对动态规划又不太了解,所以我又把动态规划的思路学习了一遍,很可惜,我仍然觉得学的并不透彻。主要的参考有:

  1. 算法导论(第三版)第十五章 的刚条切割和最长公共子序列
  2. 麻省理工学院公开课:算法导论 动态规划,最长公共子序列 
  3. 01背包问题 :该题和01背包问题极其相似
  4. 程序员面试100题之十五:数组分割 

实际上上面第四个参考的博客已经将这道题讲的非常透彻了,但是我对本题还有一点小小的想法。

一个动态规划的算法设计如下,摘自算法导论 P204

  1. 刻画一个最优解的结构特征
  2. 递归地定义最有解的值
  3. 计算最有解的值,通常采用自底向上的方法
  4. 利用计算出的信息构造一个最有解

另外,动态规划的时间复杂度我发现都是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(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最近的那个值,也就是子数组的和的大小。那么子数组该如何得到呢?

其它问题

C语言动态申请二维数组

(转)用malloc动态分配二维数组 

    原文作者:zy825316
    原文地址: https://blog.csdn.net/zy825316/article/details/22670665
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞