数组分割

问题:

1. 有一个无序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为两个子数组,子数组的元素个数不限,并使两个子数组之和最接近。


1. 解法1:

由于对两个子数组和最接近的判断不太直观,我们需要对题目进行适当转化。我们知道当一个子数组之和最接近原数组之和sum的一半时,两个子数组之和是最接近的。所以转化后的题目是:从2n个数中选出任意个数,其和尽量接近于给定值sum/2。


这个问题存储的是从前k个数中选取任意个数,且其和为s的取法是否存在dp[k][s]。之所以将选出的数之和放在下标中,而不是作为dp[k]的值,是因为那种做法不满足动态规划的前提——最优化原理,假设我们找到最优解有k个数p1p2...pk(选出的这k个数之和是最接近sum/2的),但最优解的前k-1个数p1p2...pk-1之和可能并不是最接近sum/2的,也就是说可能在访问到pk之前有另一组数q1q2....qk-1其和相比p1p2...pk-1之和会更接近sum/2,即最优解的子问题并不是最优的,所以不满足最优化原理。因此我们需要将dp[k]的值作为下标存储起来,将这个最优问题转化为判定问题,用带动态规划的思想的递推法来解。


外阶段:在前k1个数中进行选择,k1=1,2...2*n。
内阶段:从这k1个数中任意选出k2个数,k2=1,2...k1。

状态:这k2个数的和为s,s=1,2...sum/2。

决策:决定这k2个数的和有两种决策,一个是这k2个数中包含第k1个数,另一个是不包含第k1个数。
dp[k][s]表示从前k个数中取任意个数,且这些数之和为s的取法是否存在。

#include <iostream>
#include <algorithm>

using namespace std;

#define MAXN 101
#define MAXSUM 100000
int A[MAXN];
bool dp[MAXN][MAXSUM];

// dp[k][s]表示从前k个数中去任意个数,且这些数之和为s的取法是否存在
int main()
{
	int n, i, k1, k2, s, u;
	cin >> n;
	for (i=1; i<=2*n; i++)
		cin >> A[i];
	int sum = 0;
	for (i=1; i<=2*n; i++)
		sum += A[i];
	memset(dp,0,sizeof(dp));
	dp[0][0]=true;
	// 外阶段k1表示第k1个数,内阶段k2表示选取数的个数
	for (k1=1; k1<=2*n; k1++)			// 外阶段k1
	{
		for (k2=k1; k2>=1; k2--)		// 内阶段k2
			for (s=1; s<=sum/2; s++)	// 状态s
			{
				//dp[k1][s] = dp[k1-1][s];
				// 有两个决策包含或不包含元素k1
				if (s>=A[k1] && dp[k2-1][s-A[k1]])
					dp[k2][s] = true;
			}
	}
	// 之前的dp[k][s]表示从前k个数中取任意k个数,经过下面的步骤后
	// 即表示从前k个数中取任意个数
	for (k1=2; k1<=2*n; k1++)
		for (s=1; s<=sum/2; s++)
			if (dp[k1-1][s]) dp[k1][s]=true;
	// 确定最接近的给定值sum/2的和
	for (s=sum/2; s>=1 && !dp[2*n][s]; s--);
	printf("the differece between two sub array is %d\n", sum-2*s);
}

解法2:

由于题目不限制子数组的元素个数,限制条件少,可以进行优化。实际上解法1的思路主要是为了题目2做铺垫,使得题目2的解法不至于太难理解。该题实际上有更简单的解法,该解法的思路和0-1背包问题的思路是一样的。

#include <iostream>
using namespace std;


#define MAXN 101
#define MAXSUM 100000
int A[MAXN];
bool dp[MAXN][MAXSUM];


// dp[k][s]表示从前k个数中取任意个数,且这些数之和为s的取法是否存在
int main()
{
	int k, s, u, i, n;
	cin >> n;
	for (i=1; i<=2*n; ++i)
		cin >> A[i];
	int sum = 0;
	for (i=1; i<=2*n; ++i)
		sum += A[i];
	dp[0][0] = true;
	// 阶段k表示第k个数
	for (k=1; k<=2*n; ++k)
		// 注意状态可取0
		for (s=0; s<=(sum>>1); ++s)
		{
			// 加上第k个数,或不加它所能得到的和
			if (s>=A[k])
				dp[k][s] = dp[k-1][s-A[k]] || dp[k-1][s];
			else
				dp[k][s] = dp[k-1][s];
		}
	for (s=(sum>>1); s>=1 && !dp[2*n][s]; --s);
	cout << sum-2*s;
}
 

2. 有一个无序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组之和最接近。


但本题还增加了一个限制条件,即选出的物体数必须为n,这个条件限制了内阶段k2的取值范围,并且dp[k][s]的含义也发生变化。这里的dp[k][s]表示从前k个数中取任意个数,且不超过n,且这些数之和为s的取法是否存在。


#include <iostream>
#include <algorithm>

using namespace std;

#define MAXN 101
#define MAXSUM 100000
int A[MAXN];
bool dp[MAXN][MAXSUM];

// 题目可转换为从2n个数中选出n个数,其和尽量接近于给定值sum/2
int main()
{
	int n, i, k1, k2, s, u;
	cin >> n;
	for (i=1; i<=2*n; i++)
		cin >> A[i];
	int sum = 0;
	for (i=1; i<=2*n; i++)
		sum += A[i];
	memset(dp,0,sizeof(dp));
	dp[0][0]=true;
	// 对于dp[k][s]要进行u次决策,由于阶段k的选择受到决策的限制,
	// 这里决策选择不允许重复,但阶段可以重复,比较特别
	for (k1=1; k1<=2*n; k1++)				// 外阶段k1
		for (k2=min(k1,n); k2>=1; k2--)		// 内阶段k2
			for (s=1; s<=sum/2; s++)	// 状态s
				// 有两个决策包含或不包含元素k1
				if (s>=A[k1] && dp[k2-1][s-A[k1]])
					dp[k2][s] = true;
	// 确定最接近的给定值sum/2的和
	for (s=sum/2; s>=1 && !dp[n][s]; s--);
	printf("the differece between two sub array is %d\n", sum-2*s);
}
——————————————————————————————————————————————————

本文说是《编程之美》2.18新思路,其实也是July的《微软等公司面试100题》上的32题的解法。

 

两个序列大小均为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最小。

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

        

         首先对于两个素个数相等的序列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]的值。

        伪代码如下:

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[][][]来记录所选择元素的轨迹。含路径的伪代码如下,只是在上述伪代码中添加了一点代码而已。

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]逆着推导来打印轨迹对应的元素。伪代码如下:

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][]已经变化,那么计算的结果就是错误的。

       伪代码如下

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的空间来存放轨迹。即

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个数,拆成两个数组,使得这两个数组和之差最小。

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


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

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

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)

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

测试代码

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





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值