【编程之美】2.18 数组分割

题目:

有一个无序、元素个数为2n的正整数数组,要求:如何能吧这个数组分割为元素个数为n的两个数组,并使两个子数组的和最近?

例如有如下数组如图:

 

思路:

编程之美的书上解法一中说我们直观的思路是对所有元素排序

S:a0 < a1 < a2 ... < a2n-1

然后分为

S1 = [a1, a3, ...,a2n-1]

S2 = [a0, a2, ...,a2n-2]

我的思路的前面也是这样的,不过后面有加了一步:

对S数组 分为n组[a0 a1][a2 a3][a4 a5]...[a2n-2 a2n-1]

每一组的数字都是相邻的 然后我们对这个S按照组来排序, 把每一组中数字差值大的组放在前面, 差值小的放在后面

然后用diff记录S1-S2的变化,如果diff < 0 则说明前面的和S1偏小,那么我们就把当前组的S1和S2的数字交换 减小diff

如果diff大于等于0,由于S1原本的数字就是较小的,不用交换,直接更新diff就可以了。

而且:由于我们每次都是填充差距最大的对,两个组的和之差只会越来越小。

------------------

注:虽然没有证明其正确性,但是我也举不出反例来。对于有负数的情况代码依然运转的很好。 时间复杂度也只有O(NlogN),空间复杂度也只有O(N),因为除了排序,我完全是在原地操作的。 比书上解法二和解法三的效率要高得多。

question:谁能帮我举出这种思路无法正确处理的反例吗?

答:突然发现,下面的例子里面就有反例

   50  100

   40  30

   49  48

   45  44

   22  21

sum:206 243

其实把左右的22 和 48 换一下 差值就变小了。唉,果然简便算法不对,还是老老实实看书吧。

------------

代码如下:

//start time 10:45
//end time 11:30

#include <stdio.h>
#include <stdlib.h>


int cmp1(const void * p1, const void * p2)
{
    return (*(int *)p1) - (*(int *)p2); //从小到大排序
}
int cmp2(const void * p1, const void * p2)
{
    int a = *((int*)p1 + 1) - *(int*)p1;
    int b = *((int*)p2 + 1) - *(int*)p2;
    return b - a; //按照数值差 从大到小排序
}

void getClosedTwoArray(int * a, int alen)
{
    if(alen & 0x1 == 1)
    {
        printf("input error alen must be an even num!\n");
        return;
    }
    //从小到大排序
    qsort(a, alen, sizeof(a[0]), cmp1);
    //a中两个相邻元素为一组 按照每组元素的差值的绝对值 从大到小排序
    qsort(a, alen >> 1, 2 * sizeof(a[0]), cmp2);
    int diff = 0; //偶数位数组 减 奇数位数组的差值
    for(int i = 0; i < (alen >> 1); i++)
    {
        //如果之前偶数位的数组和小于奇数位的数组和 则交换两个的位置 减小差距 
        //由于按照差值从大到小排列的 所以每次都先填充大的差距
        if(diff < 0)
        {
            int tmp = a[2 * i];
            a[2 * i] = a[2*i + 1];
            a[2*i + 1] = tmp;
        }
        diff = diff + a[2*i] - a[2*i + 1];
    }
    int sum1 = 0, sum2 = 0;
    for(int i = 0; i < (alen >> 1); i++)
    {
        printf("    %d    %d\n", a[2*i], a[2*i + 1]);
        sum1 += a[2*i];
        sum2 += a[2*i + 1];
    }
    printf("sum: %d    %d\n", sum1, sum2);
}

int main()
{
    int a[10] = {3,26,-8,12,9,30,7,11,20,17};
    int b[12] = {100,99,98,1,2,3,1,2,3,4,5,40};
    int c[8] = {5,5,9,10,4,7,7,13};
    int d[8] = {5,5,109,110,1,10,107,113};
    int e[10] = {1,5,7,8,9,6,3,11,20,17};
    int f[10] = {100,50,49,48,45,44,40,30,22,21};
    getClosedTwoArray(f, 10);
    return 0;
}

 

把书上的正确答案记录一下:

我偷懒,直接copy人家已经写好了的http://www.cnblogs.com/pangxiaodong/archive/2011/10/10/2205366.html

    方法二,动态规划,原题是要求求两个数组的和最近接,这等价于要求其中较小的和最接近与2n个正整数的和(设为SUM)的一半。因此,弱化题目,求这个最近接一半的且小于等于SUM/2数值,定义Heap[i],i<=N表示任意i个数能够构成的数值集合。初始化:Heap[0]= 0。更新代码:

for(int i=1; i<2*N; i++) { // 依次读取A[i]更新堆 
for(int j=min{i, N}; j>0; j--) // 更新引入A[i]后可能的元素个数的情况
for each v in Heap[j-1] // 对于引入A[i]的情况
insert(Heap[j], A[i]+v);
}

    关于insert次数,至多为2^(N-1)。

    方法三,复杂度主要是由于堆很大,原因是我们记录的是各种可能组合出的数值。如果SUM值不高,可以定义bool FLAG[i][j],i=0,1,...,2*N,j=0,1,...,SUM/2,表示是否存在i个数,其和为j。初始化,FLAG[0][0] = true。

复制代码
for(int k=1; k<2*N; k++) {
for(int i=min{k, N}; i>0; i--) { // 每一个可能的长度
for(int v=1; v<SUM/2; v++) { // 每一个可能的可能的数值
if(v>=A[k] && FLAG[i-1][v-A[k]])
FLAG[i-1][v-A[k]] = true;
}
}
}
复制代码

    Max{j},其中,j=0,1,...,2*N,FLAG[N][j]=true,这即为所求。这样复杂度为O(N*N*SUM)级别的,尤其是当SUM相对不大的时候,复杂度会大大降低。

posted @ 2014-11-03 13:29  匡子语  阅读(906)  评论(0编辑  收藏  举报