编程之美2.18_由数组分割到背包问题(1)

编程之美上2.18题:

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

eg:1,5,7,8,9,6,3,11,20,17->1,3,11,8,20     5,7,9,6,17

 

分析:

题目的本质是从2n个整数中找出n个数,使得其和最接近所有整数总和的一半。

编程之美的解法一已经明确了,该种揭发不是最优的。

下面两种解法主要是用动态规划,虽然我看的不是很明白,但是这让我想到了这题可以用背包问题来求解。背包问题是标准的动态规划问题。下面是思路:

 

由题目看,此题应该转换为01背包问题,因为其中的元素每个只能取一次。背包的容量W为sum/2,物品的总个数M=2n。但是01背包问题只需要求得装入背包的物品使得总价值最大即可,对装入物品的个数并没有限制。但是这里不同,装入背包的物品总价值不仅要最大,而且装入的物品个数一定是2n的一半:n个!这就需要好好的思考一下了~

 

我们假设F[i][j][k]表示从前i个数中取j个数,其和不超过k且最接近k的一个值。

那么利用01背包的思想:

F[i][j][k] = max(F[i-1][j][k],F[i-1][j][k-A[i]]+A[i]),其中1<=i<=2n,1<=j<=min(i,n),1<=k<=sum/2,前一个式子表示第1个数不取,后一个式子表示取第i个数。这样,最后取的n个数的总和结果就是F[2n][n][sum/2]。注意,前后两个式子表示的情况是完备且互斥的

另外有一个很关键的是求到底取了数组中的读几个数,当F[i][j][k] = F[i-1][j][k-A[i]]+A[i]时,说明第i个数被取,这时候需要一个数组P[][][]进行记录。可以这么做:

当F[i][j][k] = F[i-1][j][k-A[i]]+A[i]时,令P[i][j][k] = 1,最后从F[2n][n][sum/2]逆着走向F[0][0][0,],若发现F[i][j][k]= 1, 则说明数组中的第i个数取了,同时k = k-A[i],j = j-1,i = i-1。

好了,直接上代码:

#include <iostream>
#include <algorithm>
using namespace  std;

int F[100][50][1000];
int P[100][50][1000];

int GetSplitArraySum(int *arr, int n)//n代表元素个数不包括第0个
{
	int sum = 0;
	for (int i=1; i<=n; ++i)
		sum += arr[i];
	int val = sum>>1;

	for (int i=1; i<=n; ++i) {
		for (int j=1; j<=min(i, n>>1); ++j) {
			for (int k=1; k<=val; ++k) {
				F[i][j][k] = F[i-1][j][k]; 
				if (k>=arr[i] && F[i][j][k] < F[i-1][j-1][k-arr[i]]+arr[i])  {
					F[i][j][k]=F[i-1][j-1][k-arr[i]]+arr[i] ;
					P[i][j][k] = 1;
				}
			}
		}
	}


	return F[n][n>>1][val];
}

void PrintElem(int *arr, int n)
{
	int sum = 0;
	for (int i=1; i<=n; ++i)
		sum += arr[i];
	int val = sum>>1;

	int i= n;
	int j = n>>1;
	int k = val;
	while (i>0 && j>0 && k>0) {
		if (P[i][j][k]==1) {
			cout<<arr[i]<<" ";
			--j;
			k -= arr[i];
		} 
		--i;
	}
	cout<<endl;

}
int main(int argc, char **argv)
{
	int arr[10+1] = {0,1,5,7,8,9,6,3,11,20,17};
	cout<<GetSplitArraySum(arr, 10)<<endl;
	PrintElem(arr, 10);

	system("pause");
	return 0;
}

此方法的空间复杂度为O(2N*N*Sum/2)即O(N^2*Sum),时间复杂度为O(N2Sum)。还可以优化么?待续~


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值