zoukankan      html  css  js  c++  java
  • 【编程之美】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!
    ");
            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
    ", a[2*i], a[2*i + 1]);
            sum1 += a[2*i];
            sum2 += a[2*i + 1];
        }
        printf("sum: %d    %d
    ", 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相对不大的时候,复杂度会大大降低。

  • 相关阅读:
    吊打XXX
    [CQOI2011]动态逆序对
    陌上花开
    【BOI2007】摩基亚Mokia
    [SCOI2008]奖励关
    最小生成树
    打表
    【中学高级本】倒酒
    整数合并
    韩信点兵
  • 原文地址:https://www.cnblogs.com/dplearning/p/4070998.html
Copyright © 2011-2022 走看看