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

【题目】

任意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[][][]来记录所选择元素的轨迹。

该算法的时间复杂度为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 = sum / 2;k >= 1;--k){
                // dp[i][j][k] = max(dp[i-1][j][k],dp[i-1][j-1][k-num[i]] + num[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;
    }
时间: 2024-08-10 16:14:02

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

经典面试题 之 数组的循环右移

经典面试题 之 数组的循环右移 题目的大意是将一个长度为n的数组A内的元素循环右移m位(当然左移也可以),比如数组 {1, 2, 3, 4, 5}右移3位之后就变成{3, 4, 5, 1, 2}. 这题最平凡的做法是开另一个大小一样的数组B,遍历一下,令B[(i + m) % n] = A[i],再将B的内容写回到A即可.这个方法的时间复杂度为O(N),空间复杂度也为O(N). 很明显,需要优化空间的使用.有一种很优美但不太好懂的方法,是先将A的元素倒置,即{1, 2, 3, 4, 5}变成{5

[经典面试题][百度]数组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]相差很大)的情况下或许可以

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

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

【转】嵌入式软件工程师经典笔试题

嵌入式软件工程师经典笔试题 > 预处理器(Preprocessor) 1. 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL 我在这想看到几件事情: 1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等) 2). 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中 有多少秒而不是计算出实际的值,是更清晰而没有代价的. 3).

Linux 经典面试题

[Linux  经典面试题] 1. 在Linux系统中,以 文件 方式访问设备 . 2. Linux内核引导时,从文件 /etc/fstab 中读取要加载的文件系统. 3. Linux文件系统中每个文件用 i节点 来标识. 4. 全部磁盘块由四个部分组成,分别为引导块 .专用块 . i节点表块 和数据存储块. 5. 链接分为: 硬链接 和 符号链接 . 6. 超级块包含了i节点表 和 空闲块表 等重要的文件系统信息. 7. 某文件的权限为:d-rw-_r--_r--,用数值形式表示该权限,则该八

【转】.net 经典面试题

[转].net 经典面试题 1. 简述 private. protected. public. internal 修饰符的访问权限. 答 . private : 私有成员, 在类的内部才可以访问. protected : 保护成员,该类内部和继承类中可以访问. public : 公共成员,完全公开,没有访问限制. internal: 在同一命名空间内可以访问. 2 .列举ASP.NET 页面之间传递值的几种方式. 答. 1.使用QueryString, 如....?id=1; response.

50个C/C++经典面试题

C/C++经典面试题  面试题1:变量的声明和定义有什么区别 为变量分配地址和存储空间的称为定义,不分配地址的称为声明.一个变量可以在多个地方声明, 但是只在一个地方定义.加入extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分 定义. 说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间, 如外部变量. 面试题2 :写出bool .int. float .指针变量与 “零值”比较的if 语句 bool 型数据: if( flag ) { A;

转:嵌入式软件工程师经典笔试题

> 预处理器(Preprocessor) 1. 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)  #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL 我在这想看到几件事情: 1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等) 2). 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的. 3). 意识到这个表达式将使一个1

【转载】经典10道c/c++语言经典笔试题(含全部所有参考答案)

经典10道c/c++语言经典笔试题(含全部所有参考答案) 1. 下面这段代码的输出是多少(在32位机上). char *p; char *q[20]; char *m[20][20]; int (*n)[10]; struct MyStruct { char dda; double dda1; int type ; }; MyStruct k; printf("%d %d %d %d %d",sizeof(p),sizeof(q),sizeof(m),sizeof(n),sizeof(