[经典面试题][网易]数组分割

【题目】

任意2N个正整数,从其中选出N个整数,使得选出的N个整数和同剩下的N个整数之和的差最小。

【来源】

网易

【分析】

假设数组A[1..2N]所有元素的和是SUM。模仿动态规划解0-1背包问题的策略。

从2N个数中找N个元素,有三种可能:大于Sum/2,小于Sum/2以及等于Sum/2。而大于Sum/2与小于等于Sum/2没区别,故可以只考虑小于等于Sum/2的情况。

令S(k, i)表示前k个元素中任意i个元素的和的集合。显然:

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) }

按照这个递推公式来计算,最后找出集合S(2N, N)中与SUM/2最接近的那个和,这便是答案。

【分析一】

状态转移方程:

其中,S[i-1][j][k]表示前i-1个元素中选取j个使其和不超过但最逼近k;

         S[i-1][j-1][k-A[i]]在前i-1个元素中选取j-1个元素使其和不超过但最逼近k-A[i],这样再加上A[i]即第i个元素就变成了 选择上第i个元素的情况下最逼近k的和。

而第一种情况与第二种情况是完备且互斥的,所以需要将两者最大的值作为F[i][j][k]的值。

可以设置一个三维数组Path[][][]来记录所选择元素的轨迹。根据求得的Path[][][]我们可以从S[2N][N][Sum/2]往S[0][0][0]逆着推导来打印轨迹对应的元素。

该算法的时间复杂度为O(n^2*sum),空间复杂度也为O(n^2*sum)。

【代码一】

    /*********************************
    *   日期:2015-02-01
    *   作者:SJF0115
    *   题目: 数组分割
    *   来源:网易
    *   博客:
    **********************************/
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;

    // 模仿动态规划解0-1背包问题的策略
    int MinSum(int num[],int n){
        if(n <= 0){
            return 0;
        }//if
        int sum = 0;
        int size = 2*n;
        // the sum of all number
        for(int i = 1;i <= size;++i){
            sum += num[i];
        }//for
        int dp[size + 1][n + 1][sum / 2 + 1];
        // 选中数字路径
        int path[size + 1][n + 1][sum / 2 + 1];
        memset(dp,0,sizeof(dp));
        memset(path,0,sizeof(path));
        // 用dp(i,j,k)来表示从前i个元素中取j个元素,使得其和不超过k且最接近k,j <= min(i,n),k <= Sum/2
        // 状态转移方程:  
        // dp(i,j,k)= max{dp(i-1,j-1,k-num[i])+num[i],dp(i-1,j,k)}
        // dp(2N,N,SUM/2+1)就是题目的解。
        // 前i个元素
        for(int i = 1;i <= size;++i){
            // 任选j个元素
            int count = min(i,n);
            for(int j = 1;j <= count;++j){
                // 使得其和不超过k且最接近k
                for(int k = 1;k <= sum / 2;++k){
                    // dp[i][j][k] = max(dp[i-1][j][k],dp[i-1][j-1][k-num[i]] + num[i]);
                    // 是否选第i个元素
                    dp[i][j][k] = dp[i-1][j][k];
                    if(k >= num[i] && dp[i-1][j][k] < dp[i-1][j-1][k-num[i]] + num[i]){
                        dp[i][j][k] = dp[i-1][j-1][k-num[i]] + num[i];
                        path[i][j][k] = 1;
                    }//if
                }//for
            }//for
        }//for
        // 打印选择路径
        int i = 2*n,j = n,k = sum / 2;
        while(i > 0 && j > 0, k > 0){
            if(path[i][j][k] == 1){
                cout<<num[i]<<" ";
                --j;
                k -= num[i];
            }//if
            --i;
        }//while
        cout<<endl;
        int sum1 = dp[size][n][sum/2];
        int sum2 = sum - dp[size][n][sum/2];
        cout<<"Sum1->"<<sum1<<" sum2->"<<sum2<<endl;
        return abs(sum1 - sum2);
    }

    int main(){
        //int A[] = {0,1,5,7,8,9,6,3,11,20,17};
        //int A[] = {0,1,2,3,4,5,6,7,8,9,10};
        int A[] = {0,2,8,5,10};
        int n = 2;
        cout<<"最小差->"<<MinSum(A,n)<<endl;;
        return 0;
    }

【分析二】

我们从上面的代码可以看出,S[i][j][k]只与 S[i-1][][]有关,这一点状态方程上也能反映出来。这种固定关系我们可以想办法去掉。从而节省空间。

我们可以用二维数组来代替三维数组来达到降低空间复杂度的目的。

但是怎么代替呢?因为S[i][j][k]只与S[i-1][][]有关,可以把i这一维省略。

同时用二维数组来代替的时候应该对S[i][j][k]的“j”维进行逆序遍历。

为 什么? 因为只有这样才能保证计算S[i][j][k]时利用的S[i-1][j][]和S[i-1][j-1][]是真正i-1这个状态的值,如果正序遍 历,那么当计算S[][j][]时,S[][j-1][]已经变化,那么计算的结果就是错误的。

【代码二】

    /*********************************
    *   日期:2015-02-01
    *   作者:SJF0115
    *   题目: 数组分割
    *   来源:网易
    *   博客:
    **********************************/
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;

    int MinSum(int num[],int n){
        if(n <= 0){
            return 0;
        }//if
        int sum = 0;
        int size = 2*n;
        // the sum of all number
        for(int i = 1;i <= size;++i){
            sum += num[i];
        }//for
        int dp[n + 1][sum / 2 + 1];
        // 选中数字路径
        int path[size+1][n + 1][sum / 2 + 1];
        memset(dp,0,sizeof(dp));
        memset(path,0,sizeof(path));
        //
        for(int i = 1;i <= size;++i){
            // 任选j个元素
            int count = min(i,n);
            for(int j = count;j >= 1;--j){
                // 使得其和不超过k且最接近k
                for(int k = num[i-1];k <= sum / 2;++k){
                    if(dp[j][k] < dp[j-1][k-num[i-1]] + num[i-1]){
                        dp[j][k] = dp[j-1][k-num[i-1]] + num[i-1];
                        path[i][j][k] = 1;
                    }//if
                }//for
            }//for
        }//for
        // 打印选择路径
        int i = 2 * n,j = n,k = sum / 2;
        while(i > 0 && j > 0 && k > 0){
            if(path[i][j][k] == 1){
                cout<<num[i]<<" ";
                --j;
                k -= num[i];
            }//if
            --i;
        }//while
        cout<<endl;
        int sum1 = dp[n][sum/2];
        int sum2 = sum - dp[n][sum/2];
        cout<<"Sum1->"<<sum1<<" sum2->"<<sum2<<endl;
        return abs(sum1 - sum2);
    }

    int main(){
        int A[] = {0,1,5,7,8,9,6,3,11,20,17};
        //int A[] = {0,1,2,3,4,5,6,7,8,9,10};
        //int A[] = {0,2,8,5,10};
        int n = 5;
        cout<<"最小差->"<<MinSum(A,n)<<endl;;
        return 0;
    }

【思路三】

    /*-------------------------------------
    *   日期:2015-02-01
    *   作者:SJF0115
    *   题目: 数组分割
    *   来源:网易
    *   博客:
    ------------------------------------*/
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;

    int MinSum(int num[],int n){
        if(n <= 0){
            return 0;
        }//if
        int sum = 0;
        int size = 2*n;
        // the sum of all number
        for(int i = 1;i <= size;++i){
            sum += num[i];
        }//for
        // isOK[i][v]表示是否可以找到i个数,使得它们之和等于v
        int isOK[n + 1][sum / 2 + 1];
        int path[size + 1][n + 1][sum / 2 + 1];
        // 都不合法
        memset(isOK,0,sizeof(isOK));
        memset(path,0,sizeof(path));
        // 注意初始化
        // 可以,取0件物品,总合为0,是合法的
        isOK[0][0] = 1;
        for(int i = 1;i <= size;++i){
            int count = min(i,n);
            for(int j = 1;j <= count;++j){
                // 从大到小,数组少了一维
                for(int k = sum / 2;k >= num[i];--k){
                    if( isOK[j-1][k-num[i]] ){
                        isOK[j][k] = 1;
                        path[i][j][k] = 1;
                    }//if
                }//for
            }//for
        }//for
        // 打印选择路径
        int i = 2 * n,j = n,k = sum / 2;
        while(i > 0 && j > 0 && k > 0){
            if(path[i][j][k] == 1){
                cout<<num[i]<<" ";
                --j;
                k -= num[i];
            }//if
            --i;
        }//while
        cout<<endl;
        //
        int minNum;
        for(int k = sum / 2;k >= 0;--k){
            if(isOK[n][k]){
                minNum = k;
                break;
            }//if
        }//for
        int sum1 = minNum;
        int sum2 = sum - minNum;
        cout<<"Sum1->"<<sum1<<" sum2->"<<sum2<<endl;
        return abs(sum1 - sum2);
    }

    int main(){
        //int A[] = {0,1,5,7,8,9,6,3,11,20,17};
        //int A[] = {0,1,2,3,4,5,6,7,8,9,10};
        int A[] = {0,2,8,5,10};
        int n = 2;
        cout<<"最小差->"<<MinSum(A,n)<<endl;;
        return 0;
    }
时间: 2024-11-03 19:25:36

[经典面试题][网易]数组分割的相关文章

[经典面试题][百度]数组A中任意两个相邻元素大小相差1,在其中查找某个数。

题目 数组A中任意两个相邻元素大小相差1,现给定这样的数组A和目标整数t,找出t在数组A中的位置.如数组:[1,2,3,4,3,4,5,6,5],找到4在数组中的位置. 思路 这道题目最差时间复杂度也是O(N),所以重点在于能不能找到一种尽可能减少比较次数的方法. 如数组:[1,2,3,4,3,4,5,6,5],找到4在数组中的位置.4和1比较,差为3,那么即使最好情况(递增或者递减),4也就是在a[3]的位置,可以跳过a[1]a[2].这样在特定数组(目标值和a[1]相差很大)的情况下或许可以

[经典面试题]统计数组

[题目] 给定数组A,大小为n,数组元素为1到n的数字,不过有的数字出现了多次,有的数字没有出现.请给出算法和程序,统计哪些数字没有出现,哪些数字出现了多少次.能够在O(n)的时间复杂度,O(1)的空间复杂度要求下完成么? [分析] 我们知道原数组是没有排序的.如果排序了,很简单的.O(1)的空间含义,可以使用变量,但不能开辟数组或者map等来计数. 这个题目,很直接的解法就是两层遍历,O(n^2)的复杂度,O(1)的空间.空间满足了,但是时间没有. 很多类似的题目,都会用XOR的方法,大家仔细

[经典面试题]排序数组中绝对值最小元素

[题目] 题目为: 有一个已经排序的数组(升序),数组中可能有正数.负数或0,求数组中元素的绝对值最小的数,要求,不能用顺序比较的方法(复杂度需要小于O(n)),可以使用任何语言实现 例如,数组{-20,-13,-4, 6, 77,200} ,绝对值最小的是-4. [分析] 给定数组是已经排好序的,且是升序,没有重复元素. 一个简单的思路,就是一次性遍历数组,求出数组的元素的绝对值的最小值,这样的时间复杂度为O(n). 但是,这样就浪费了题目的一个条件:数组是已经排好序的.所以,需要对原来的题目

PHP经典面试题集锦

 这篇文章主要介绍了PHP经典面试题集锦,搜集整理了常见的php面试题与相关的参考答案,供大家参考借鉴,需要的朋友可以参考下     本文较为详细的分析了PHP经典面试题.分享给大家供大家参考.具体如下: 做了一下网络上的php题目,不知不觉做到现在.....把答案贴出来,供参考之用. 1.用PHP打印出前一天的时间格式是2006-5-10 22:21:21(2分) ? 1 2 $a = date("Y-m-d H:i:s", strtotime("-1 day")

[经典面试题]子数组的最大乘积

题目 给定一个长度为N的整数数组,只允许用乘法,不能用除法,计算任意(N-1)个数的组合乘积中的最大的一组,并写出算法的时间复杂度. 思路一 穷举法 我们把所有可能的(N-1)个数的组合找出来,分别计算它们的乘积,并比较大小.由于总共有N个(N-1)个数的组合,总的时间复杂度为O(N^2),但显然这不是最好的思路. 思路二 空间换时间 计算(N-1)个数的组合乘积,假设第i个(0<=i<=N-1)元素被排除在乘积之外(如下图). 设num[]为初试数组 Left[i]表示前i个元素(包括第i个

[经典面试题]二分查找问题汇总

[算法]二分查找算法 1.[给定一个有序(非降序)数组A,可含有重复元素,求最小的i使得A[i]等于target,不存在则返回-1.] [题目] 给定一个有序(非降序)数组A,可含有重复元素,求最小的i使得A[i]等于target,不存在则返回-1. [分析] 此题也就是求target在数组中第一次出现的位置.这里可能会有人想先直接用原始的二分查找,如果不存在直接返回-1, 如果存在,然后再顺序找到这个等于target值区间的最左位置,这样的话,最坏情况下的复杂度就是O(n)了,没有完全发挥出二

[经典面试题]最大连续乘积

题目 给一个浮点数序列,取最大乘积连续子串的值,例如 -2.5,4,0,3,0.5,8,-1,则取出的最大乘积连续子串为3,0.5,8.也就是说,上述数组中,3 0.5 8这3个数的乘积3*0.5*8=12是最大的,而且是连续的. 分析 最大乘积连续子串与最大乘积子序列不同,前者子串要求连续,后者子序列不要求连续.初看此题,自然会想到最大连续乘积问题类似于最大子数组和问题 思路一 穷举法 穷举子串的起点和终点. 代码一 /*------------------------------------

PHP经典面试题集锦_php技巧

本文较为详细的分析了PHP经典面试题.分享给大家供大家参考.具体如下: 做了一下网络上的php题目,不知不觉做到现在.....把答案贴出来,供参考之用. 1.用PHP打印出前一天的时间格式是2006-5-10 22:21:21(2分) $a = date("Y-m-d H:i:s", strtotime("-1 day")); print_r($a); 2.echo(),print(),print_r()的区别(3分) echo 和print不是一个函数,是一个语言

[经典面试题]在O(1)时间删除链表结点

[题目] 给定链表的头指针和一个结点指针,在O(1)时间删除该结点.链表结点的定义如下: struct ListNode {     int        value;     struct ListNode*  next; }; 函数的声明如下: void DeleteNode(ListNode* head,ListNode* node); [思路] 这是一道广为流传的Google面试题,能有效考察我们的编程基本功,还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解. 在链表中