数组分割问题(转)

题记:这道题和《编程之美》一书中2.18节的数组分割区别不大,但本人觉得《编程之美》这一节讲的不够透彻,不好理解(或许本人愚钝),故给出自己的思路,同时也给出打印其中一种方案的方法(这一点《编程之美》并没有提到)。

两个序列大小均为n,序列元素的值为任一整数,无序;

要求通过交换两个序列的元素,使序列a元素之和与序列b的元素之和的差最小(可能存在很多种组合,要求找出其中一种即可)。

如序列:1  5   7   8   9和序列6  3   11  20  17我们可以通过交换得到新的序列1  5   9   8   20和序列7   6   3   11  17,前者和为43,后者和为44,两者之差为1最小。

首先对于两个素个数相等的序列a、b我们利用《编程之美》2.18节的思想,将其合并为一个序列便于后续操作,序列中有负数的情况我们可以先预处理一下, 让每个元素都加上一个初始值使得最后每个元素都为正。整个问题就转化为在一个元素个数为2n的正数数组中找出其中n个元素,使得这n个元素之和与剩下元素 之和的差最小。

《编程之美》2.18解法二中提到,从2n个数中找n个元素,有三种可能:大于Sum/2,小于Sum/2以及等于Sum/2。而大于Sum/2与小于等于Sum/2没区别,故可以只考虑小于等于Sum/2的情况,这一点我们仍然沿用这个思想。

下面谈谈变化的东西:

同样,利用动态规划的思想:

先给一个空间复杂度为O(2N*N*Sum/2)即O(N2Sum)的方法,下面会对空间复杂度进行优化:

设F[i][j][k]表示前i个元素中选取j个元素,使得其和不超过k且最接近k。那么可以根据第i个元素是否选择来进行决策

状态方程如下:

         1-1

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

F[i-1][j-1][k-A[i]]在前i-1个元素中选取j-1个元素使其和不超过但最逼近k-A[i],这样再加上A[i]即第i个元素就变成了 选择上第i个元素的情况下最逼近k的和。而第一种情况与第二种情况是完备且互斥的,所以需要将两者最大的值作为F[i][j][k]的值。

伪代码如下:

[cpp] view plaincopy

  1. F[][][]← 0
  2. for i ← 1 to 2*N
  3. nLimit ← min(i,N)
  4. do for j ← 1 to nLimit
  5. do for k ← 1 to Sum/2
  6. F[i][j][k] ← F[i-1][j][k]
  7. if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
  8. then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
  9. return F[2N][N][Sum/2]

当然,前面已经提到,要给出一种方案的打印,下面我们谈谈怎么打印一种方案。

可以设置一个三维数组Path[][][]来记录所选择元素的轨迹。含路径的伪代码如下,只是在上述伪代码中添加了一点代码而已。

[cpp] view plaincopy

  1. F[][][]← 0
  2. Path[][][]← 0
  3. for i ← 1 to 2*N
  4. nLimit ← min(i,N)
  5. do for j ← 1 to nLimit
  6. do for k ← 1 to Sum/2
  7. F[i][j][k] ← F[i-1][j][k]
  8. if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
  9. then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
  10. Path[i][j][k] ← 1
  11. return F[2N][N][Sum/2] and Path[][][]

根据求得的Path[][][]我们可以从F[2N][N][Sum/2]往F[0][0][0]逆着推导来打印轨迹对应的元素。伪代码如下:

[cpp] view plaincopy

  1. i ← 2N
  2. j ← N
  3. k ← Sum/2
  4. while (i > 0 && j > 0 && k > 0)
  5. do if(Path[i][j][k] = 1)
  6. then Print A[i]
  7. j ← j-1
  8. k ← k-A[i]
  9. i ← i-1

上面的伪代码的意思是,每当找到一个Path[][][]=1,就将其对应的A[i]输出,因为已经确定一个所以j应该自减1,而k代表总和,所以也应该减去A[i]。至于为什么不管Path[][][]是否为1都需要i自减1,这一点可以参照本人博文《背包问题——“01背包”详解及实现(包含背包中具体物品的求解)》中的路径求法相关内容。

下面开始优化空间复制度为O(N*Sum/2)

我们观察前面不含路径的伪代码可以看出,F[i][j][k]只与 F[i-1][][]有关,这一点状态方程上也能反映出来。所以我们可以用二维数组来代替三维数组来达到降低空间复杂度的目的。但是怎么代替里面存有玄 机,我们因为F[i][j][k]只与F[i-1][][]有关,所以我们用二维数组来代替的时候应该对F[i][j][k]的“j”维进行逆序遍历。为 什么?因为只有这样才能保证计算F[i][j][k]时利用的F[i-1][j][]和F[i-1][j-1][]是真正i-1这个状态的值,如果正序遍 历,那么当计算F[][j][]时,F[][j-1][]已经变化,那么计算的结果就是错误的。

伪代码如下

[cpp] view plaincopy

  1. F[][]← 0
  2. for i ← 1 to 2*N
  3. nLimit ← min(i,N)
  4. do for j ← nLimit to 1
  5. do for k ← A[i] to Sum/2
  6. if (F[j][k] < F[j-1][k-A[i]]+A[i])
  7. then F[j][k] ← F[j-1][k-A[i]]+A[i]
  8. return F[N][Sum/2] and Path[][][]

上面的伪代码基本上和《编程之美》2.18节最后所给的代码基本一致了,但是里面并不含Path,如果要打印其中一种方案,那么仍需要2N*N*Sum/2的空间来存放轨迹。即

[cpp] view plaincopy

  1. F[][]← 0
  2. Path[][][]← 0
  3. for i ← 1 to 2*N
  4. nLimit ← min(i,N)
  5. do for j ← nLimit to 1
  6. do for k ← A[i] to Sum/2
  7. if (F[j][k] < F[j-1][k-A[i]]+A[i])
  8. then F[j][k] ← F[j-1][k-A[i]]+A[i]
  9. Path[i][j][k] ← 1
  10. return F[N][Sum/2] and Path[][][]

打印路径的伪代码与之前的一模一样,这里不再重写。

下面给出《编程之美》2.18节所讲的“数组分割”中给出的数据进行本文思想的C++代码实现

数组1   5   7   8   9   6   3   11  20  17一共10个数,拆成两个数组,使得这两个数组和之差最小。

[cpp] view plaincopy

  1. #include <iostream>
  2. #include <cstring>
  3. #include "CreateArray.h"    //该头文件是动态开辟及销毁二维三维数组的,读者自己实现
  4. using namespace std;

//这里参数array为整个合并后的数组序列,nLen为合并后的数组长,nToBeClosed是之前所提的Sum/2

//算法时间复杂度为O(N2Sum),空间复杂度为O(N2Sum)

[cpp] view plaincopy

  1. int AdjustArray(int array[], int nLen, int nToBeClosed)
  2. {
  3. int*** F = NULL;
  4. int*** Path = NULL;
  5. CreateThreeDimArray(F,nLen+1,nLen/2+1,nToBeClosed+1);       //创建三维数组,存放每一个状态
  6. CreateThreeDimArray(Path,nLen+1,nLen/2+1,nToBeClosed+1);    //创建三维数组,存放轨迹
  7. for(int i = 1; i <= nLen; i++)
  8. {
  9. int nLimit = min(i,nLen/2);
  10. for(int j = 1; j <= nLimit; j++)
  11. {
  12. for(int k = 1; k <= nToBeClosed; k++)
  13. {
  14. F[i][j][k] = F[i-1][j][k];
  15. if(k >= array[i-1])
  16. {
  17. if(F[i][j][k] < F[i-1][j-1][k-array[i-1]]+array[i-1])
  18. {
  19. F[i][j][k] = F[i-1][j-1][k-array[i-1]]+array[i-1];
  20. Path[i][j][k] = 1;
  21. }
  22. }
  23. }
  24. }
  25. }
  26. //打印调整后的其中一个数组
  27. int i = nLen, j = nLen/2, k = nToBeClosed;
  28. while(i > 0 && j > 0 && k > 0)
  29. {
  30. if(Path[i][j][k] == 1)
  31. {
  32. cout << array[i-1] << "\t";
  33. k -= array[i-1];
  34. j--;
  35. }
  36. i--;
  37. }
  38. cout << endl;
  39. int nRet = F[nLen][nLen/2][nToBeClosed];
  40. DestroyThreeDimArray(Path,nLen+1,nLen/2+1); //销毁轨迹表
  41. DestroyThreeDimArray(F,nLen,nLen/2+1);  //销毁状态表
  42. return nRet;
  43. }

//这里参数array为整个合并后的数组序列,nLen为合并后的数组长,nToBeClosed是之前所提的Sum/2

//算法时间复杂度为O(N2Sum),空间复杂度不含Path为O(NSum/2),含Path为O(N2Sum)

[cpp] view plaincopy

  1. int Fun2(int array[], int nLen, int nToBeClosed)
  2. {
  3. int** F = NULL;
  4. int*** Path = NULL;
  5. CreateTwoDimArray(F,nLen/2+1,nToBeClosed+1);        //创建二维状态表
  6. CreateThreeDimArray(Path,nLen+1,nLen/2+1,nToBeClosed+1);//创建三维轨迹表
  7. for(int i = 1; i <= nLen; i++)
  8. {
  9. int nLimit = min(i,nLen/2);
  10. for(int j = nLimit; j >= 1; j--)
  11. {
  12. for(int k = array[i-1]; k <= nToBeClosed; k++)
  13. {
  14. if(F[j][k] < F[j-1][k-array[i-1]]+array[i-1])
  15. {
  16. F[j][k] = F[j-1][k-array[i-1]]+array[i-1];
  17. Path[i][j][k] = 1;
  18. }
  19. }
  20. }
  21. }
  22. //打印调整后的其中一个数组
  23. int i = nLen, j = nLen/2, k = nToBeClosed;
  24. while(i > 0 && j > 0 && k > 0)
  25. {
  26. if(Path[i][j][k] == 1)
  27. {
  28. cout << array[i-1] << "\t";
  29. k -= array[i-1];
  30. j--;
  31. }
  32. i--;
  33. }
  34. cout << endl;
  35. int nRet = F[nLen/2][nToBeClosed];
  36. DestroyTwoDimArray(F,nLen/2+1); //销毁二维状态表
  37. DestroyThreeDimArray(Path,nLen+1,nLen/2+1); //销毁三维轨迹表
  38. return nRet;
  39. }

测试代码

[cpp] view plaincopy

  1. int main()
  2. {
  3. int array[] = {1,5,7,8,9,6,3,11,20,17};
  4. int nSum = 0;
  5. for(int i = 0; i < sizeof(array)/sizeof(int); i++)
  6. nSum += array[i];
  7. int nToBeClosed = nSum/2;
  8. cout << Fun(array,sizeof(array)/sizeof(int),nToBeClosed) << endl;
  9. cout << Fun2(array,sizeof(array)/sizeof(int),nToBeClosed) << endl;
  10. return 0;
  11. }
时间: 2024-10-10 10:29:39

数组分割问题(转)的相关文章

【编程之美】数组分割

有一个无序.元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近? 分析与解法 从题目中可以分析出,题目的本质就是要从2n个整数中找出n个,使得它们的和尽可能地靠近所有整数之和的一半. 解法一:不靠谱的解法 先将数组的所有元素排序,然后划分为S1 = {a1, a3, ..., a2n-1}和S2 = {a2, a4, ..., a2n}: 从S1和S2中找出一对数进行交换,使得两个集合中所有元素的和之间的差值尽可能的小,直到找不到可对换的.

数组分割问题

昨天同学问我一道关于数组分割的问题——有一个无序.元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并是两个子数组的和最接近. 假设2n个整数之和为sum.从2n个整数中找出n个元素的和,有三种可能:大于sum/2,等于sum/2,小于sum/2.可以考虑小于等于sum/2的情况.使用动态规划解决这个问题,其实这是一个NP问题,只能尽量去接近sum/2这个值. 我们可以定义dp[k][s]代表从前k个数中去任意个元素,且k小于等于n,其和为s是否存在:之所以将选出的

数组分割问题(转载)

问题: 1. 有一个无序.元素个数为2n的正整数数组,要求:如何能把这个数组分割为两个子数组,子数组的元素个数不限,并使两个子数组之和最接近. 2. 有一个无序.元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组之和最接近. 分析: 假设数组A[1..2N]所有元素的和是SUM.模仿动态规划解0-1背包问题的策略,令S(k, i)表示前k个元素中任意i个元素的和的集合.显然:                S(k, 1) = {A[i] | 1<= i

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

[题目] 任意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 <=

数组分割——解题笔记

数组分割--解题笔记 题目:有一个没有排序.元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近. 分析:这道题目可以用动态规划求解,或者说是一个典型的0,1背包问题,对于第i的数,到底是放进去还是不放,就要看放了对结果有什么影响,不放对结果又有什么影响.而结果是依据题目而言的,这道题目中的结果就是数组之和. 注意,一般动态规划数组中,需要先初始化所有i的结果,初始化可以为1,或者0. 然后,从前往后,依次得到每个i的优化结果,而且需要注意的是

编程之美2.18 数组分割 原创解O(nlogn)的时间复杂度求解:

题目:有一个无序.元素个数为2n的正整数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近? 1 1 2 -> 1 1 vs  2 看题时,解法的时间复杂度一般都大于或等于O(n^2).突然灵感一闪,发现一个新的解法,应该算是一个动态规划的过程吧,思路比较简单,请看代码.空间复杂度O(nlogn),时间复杂度O(n).但是不能确定是否适用所有正整数组,如果有错,请给出你的测试用例,谢谢! 代码如下: 1 1 #include <iostream> 2 2 #

算法之--数组分割

题目来源:编程之美2.18 有一个无序的,元素个数为2n的正整数的数组,要求: 如何能把这个数组分割为元素个数为n的两个数组,使得两个子数组的和尽量接近. 解析:因为两个子数组的和是一定的,等于整个数组的和.现在要求使得两个字数组的和尽量的接近,也就意味着要从其中选出n个数使得这n个数的和尽可能的接近sum/2,不妨设为从小于sum/2的方向接近.于是,这就是一个01背包的问题: 现在有2N个物品,每个物品的重量为A[i],有一个背包的大小为sum/2,现在从中挑选出N个物品,使得背包尽可能的被

编程之美2.18—数组分割

题目: 有一个没有排序,元素个数为2N的正整数数组.要求把它分割为元素个数为N的两个数组,并使两个子数组的和最接近. 基本思想: 假设数组A[1..2N]所有元素的和是SUM.模仿动态规划解0-1背包问题的策略,令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) }

编程之美——数组分割

一.题目概述:有一个没有排序,元素个数为2N的正整数数组.要求把它分割为元素个数为N的两个数组,并使两个子数组的和最接近.假设数组A[1..2N]所有元素的和是SUM.模仿动态规划解0-1背包问题的策略,令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) }按照这个递推公式来