题记:这道题和《编程之美》一书中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
- F[][][]← 0
- for i ← 1 to 2*N
- nLimit ← min(i,N)
- do for j ← 1 to nLimit
- do for k ← 1 to Sum/2
- F[i][j][k] ← F[i-1][j][k]
- if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
- then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
- return F[2N][N][Sum/2]
当然,前面已经提到,要给出一种方案的打印,下面我们谈谈怎么打印一种方案。
可以设置一个三维数组Path[][][]来记录所选择元素的轨迹。含路径的伪代码如下,只是在上述伪代码中添加了一点代码而已。
[cpp] view plaincopy
- F[][][]← 0
- Path[][][]← 0
- for i ← 1 to 2*N
- nLimit ← min(i,N)
- do for j ← 1 to nLimit
- do for k ← 1 to Sum/2
- F[i][j][k] ← F[i-1][j][k]
- if (k >= A[i] && F[i][j][k] < F[i-1][j-1][k-A[i]]+A[i])
- then F[i][j][k] ← F[i-1][j-1][k-A[i]]+A[i]
- Path[i][j][k] ← 1
- return F[2N][N][Sum/2] and Path[][][]
根据求得的Path[][][]我们可以从F[2N][N][Sum/2]往F[0][0][0]逆着推导来打印轨迹对应的元素。伪代码如下:
[cpp] view plaincopy
- i ← 2N
- j ← N
- k ← Sum/2
- while (i > 0 && j > 0 && k > 0)
- do if(Path[i][j][k] = 1)
- then Print A[i]
- j ← j-1
- k ← k-A[i]
- 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
- F[][]← 0
- for i ← 1 to 2*N
- nLimit ← min(i,N)
- do for j ← nLimit to 1
- do for k ← A[i] to Sum/2
- if (F[j][k] < F[j-1][k-A[i]]+A[i])
- then F[j][k] ← F[j-1][k-A[i]]+A[i]
- return F[N][Sum/2] and Path[][][]
上面的伪代码基本上和《编程之美》2.18节最后所给的代码基本一致了,但是里面并不含Path,如果要打印其中一种方案,那么仍需要2N*N*Sum/2的空间来存放轨迹。即
[cpp] view plaincopy
- F[][]← 0
- Path[][][]← 0
- for i ← 1 to 2*N
- nLimit ← min(i,N)
- do for j ← nLimit to 1
- do for k ← A[i] to Sum/2
- if (F[j][k] < F[j-1][k-A[i]]+A[i])
- then F[j][k] ← F[j-1][k-A[i]]+A[i]
- Path[i][j][k] ← 1
- return F[N][Sum/2] and Path[][][]
打印路径的伪代码与之前的一模一样,这里不再重写。
下面给出《编程之美》2.18节所讲的“数组分割”中给出的数据进行本文思想的C++代码实现
数组1 5 7 8 9 6 3 11 20 17一共10个数,拆成两个数组,使得这两个数组和之差最小。
[cpp] view plaincopy
- #include <iostream>
- #include <cstring>
- #include "CreateArray.h" //该头文件是动态开辟及销毁二维三维数组的,读者自己实现
- using namespace std;
//这里参数array为整个合并后的数组序列,nLen为合并后的数组长,nToBeClosed是之前所提的Sum/2
//算法时间复杂度为O(N2Sum),空间复杂度为O(N2Sum)
[cpp] view plaincopy
- int AdjustArray(int array[], int nLen, int nToBeClosed)
- {
- int*** F = NULL;
- int*** Path = NULL;
- CreateThreeDimArray(F,nLen+1,nLen/2+1,nToBeClosed+1); //创建三维数组,存放每一个状态
- CreateThreeDimArray(Path,nLen+1,nLen/2+1,nToBeClosed+1); //创建三维数组,存放轨迹
- for(int i = 1; i <= nLen; i++)
- {
- int nLimit = min(i,nLen/2);
- for(int j = 1; j <= nLimit; j++)
- {
- for(int k = 1; k <= nToBeClosed; k++)
- {
- F[i][j][k] = F[i-1][j][k];
- if(k >= array[i-1])
- {
- if(F[i][j][k] < F[i-1][j-1][k-array[i-1]]+array[i-1])
- {
- F[i][j][k] = F[i-1][j-1][k-array[i-1]]+array[i-1];
- Path[i][j][k] = 1;
- }
- }
- }
- }
- }
- //打印调整后的其中一个数组
- int i = nLen, j = nLen/2, k = nToBeClosed;
- while(i > 0 && j > 0 && k > 0)
- {
- if(Path[i][j][k] == 1)
- {
- cout << array[i-1] << "\t";
- k -= array[i-1];
- j--;
- }
- i--;
- }
- cout << endl;
- int nRet = F[nLen][nLen/2][nToBeClosed];
- DestroyThreeDimArray(Path,nLen+1,nLen/2+1); //销毁轨迹表
- DestroyThreeDimArray(F,nLen,nLen/2+1); //销毁状态表
- return nRet;
- }
//这里参数array为整个合并后的数组序列,nLen为合并后的数组长,nToBeClosed是之前所提的Sum/2
//算法时间复杂度为O(N2Sum),空间复杂度不含Path为O(NSum/2),含Path为O(N2Sum)
[cpp] view plaincopy
- int Fun2(int array[], int nLen, int nToBeClosed)
- {
- int** F = NULL;
- int*** Path = NULL;
- CreateTwoDimArray(F,nLen/2+1,nToBeClosed+1); //创建二维状态表
- CreateThreeDimArray(Path,nLen+1,nLen/2+1,nToBeClosed+1);//创建三维轨迹表
- for(int i = 1; i <= nLen; i++)
- {
- int nLimit = min(i,nLen/2);
- for(int j = nLimit; j >= 1; j--)
- {
- for(int k = array[i-1]; k <= nToBeClosed; k++)
- {
- if(F[j][k] < F[j-1][k-array[i-1]]+array[i-1])
- {
- F[j][k] = F[j-1][k-array[i-1]]+array[i-1];
- Path[i][j][k] = 1;
- }
- }
- }
- }
- //打印调整后的其中一个数组
- int i = nLen, j = nLen/2, k = nToBeClosed;
- while(i > 0 && j > 0 && k > 0)
- {
- if(Path[i][j][k] == 1)
- {
- cout << array[i-1] << "\t";
- k -= array[i-1];
- j--;
- }
- i--;
- }
- cout << endl;
- int nRet = F[nLen/2][nToBeClosed];
- DestroyTwoDimArray(F,nLen/2+1); //销毁二维状态表
- DestroyThreeDimArray(Path,nLen+1,nLen/2+1); //销毁三维轨迹表
- return nRet;
- }
测试代码
[cpp] view plaincopy
- int main()
- {
- int array[] = {1,5,7,8,9,6,3,11,20,17};
- int nSum = 0;
- for(int i = 0; i < sizeof(array)/sizeof(int); i++)
- nSum += array[i];
- int nToBeClosed = nSum/2;
- cout << Fun(array,sizeof(array)/sizeof(int),nToBeClosed) << endl;
- cout << Fun2(array,sizeof(array)/sizeof(int),nToBeClosed) << endl;
- return 0;
- }