编程之美2.18 数组分割问题

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

转化:即寻找一个元素个数为n的子数组,其元素的和最接近SUM/2。


解法:

编程之美上的解法并没有实质性地解决这个优化问题,即使找出了isOK数组,也不容易借此找出子数组的划分方案。

以下两种解法转自http://blog.csdn.net/wumuzi520/article/details/7028705


解法一:

设原数组为A,F[i][j][v]表示在前i个元素中任意取j个元素,元素和最接近v的元素和(略绕),则

(1) F[i][j][v] = F[i-1][j][v],v < A[i](此时无法选择元素A[i])

(2) F[i][j][v] = max{ F[i-1][j-1][v-A[i]] + A[i], F[i-1][j][v] },v >= A[i](此时可以选择元素A[i],但选择了不一定比 不选择更接近v,取最大值)

这样的问题定义满足DP的优化原则,比如(2)中,如果F[i-1][j-1][v-A[i]] 不满足最接近v-A[i],则必定存在另一种i-1中取j-1个元素的方案更进阶v-A[i],这与F[i][j][v]最接近v矛盾。

for (i = 1; i <= 2*n; i++)
	for (j = 1; j <= min(i, n); j++)
		for (v = 1; v <= sum/2; v++)
		{
			F[i][j][v] = F[i-1][j][v];
			if (v >= A[i] && F[i-1][j][v] < F[i-1][j-1][v-A[i]] + A[i])
			{
				F[i][j][v] = F[i-1][j-1][v-A[i]] + A[i];
				Path[i][j][v] = 1;
			}
		}

最终,F[2n][n][sum/2]就是元素个数为n的且和最接近sum/2的元素和,在程序中通过一个Path数组可以得到数组的划分方案。

i = 2*n;
j = n;
v = sum/2;
while (i > 0 && j > 0 && k > 0)
{
	if (Path[i][j][k] == 1)
	{
		cout << A[i] << ' ';
		j = j-1;
		k = k-A[i];
	}
	i = i-1;
}


解法二:

在解法一基础上改进,由于F[i][][]只与F[i-1][][]有关,因此,可以省去i变量以节省空间开销。

设F[j][v]表示在当前元素集合(元素集合是逐渐扩大的,当前元素集合指当前元素个数为i)中任取j个元素,元素和最接近v的元素和,则

代码可以如下编写:

for (i = 1; i <= 2*n; i++)
	for (j = min(i, n); j >= 1; j--)
		for (v = A[i]; v <= sum/2; v++)
		{
			if (F[j][v] < F[j-1][v-A[i]] + A[i])
			{
				F[j][v] = F[j-1][v-A[i]] + A[i];
				Path[i][j][v] = 1;
			}
		}

之所以这样,是因为,在最外层循环为i时,F[j][v]所存储的所有数据是只与前i-1个元素相关的,也就是隐含的为F[i-1][j][v],即为前i个元素中任意取j个元素元素和最接近v的元素和。然而要保证这一点,第二层循环中的j也必须倒着循环,因为F[j][v]是基于F[j-1][]来计算的,如果正着循环,则有可能在计算F[j][v]的时候F[j-1][v]已经是把A[i]加入之后的结果,已经是F[i][j-1][v]的结果,不能保证i-1。而倒序循环,j不会受j+1的影响,可以保证正确性。

这样,最终i已经变成2n,F[j][v]只是表示在前(所有)2n个元素中任意取j个元素元素和最接近v的元素和,F[n][sum/2]就是元素个数为n的且和最接近sum/2的元素和,同样可以通过一个Path数组可以得到数组的划分方案。


回到编程之美的解法三。

解法三:

设isOK[j][v]表示是否可以找到j个数,它们的和为v(编程之美上如是说),其实,这个isOK表示的意思是在当前元素集合中(元素集合在逐渐扩大,当前元素集合个数为i),是否可以找到j个数,它们的和为v。

for (i = 1; i <= 2*n; i++)
	for (j = min(i, n); j >= 1; j--)
		for (v = 1; v <= sum/2; v++)
		{
			if (v >= arr[i] && isOK[j-1][v-arr[i]])
			{
				isOK[j][v] = true;
			}
		}

和解法二的解释基本一样。最终的isOK[j][v]即表示在前2n个元素中,是否可以取j个,和为v。

只要把j定为n,for (v=sum/2; v>=1; v--) 当isOK[n][v]==true时循环停止,此时的v就是所要求的两个子数组的某一个数组的元素之和。

需要注意几行代码:

if (v >= arr[i] && isOK[j-1][v-arr[i]])

    isOK[j][v] = true;

这只是意味着isOK[j][v]在isOK[j-1][v-arr[i]]为true时会被赋为true,而后者不为true时,isOK[j][v]会保持原值,这也就与i-1时的状态对应。


把最开始的问题变化一下,任意分割为两个子数组,使它们的和最接近,如何分割?(即去掉了n的限制)

这个问题可以对解法二和解法三稍微加以改进来解决。

对于解法二,只要对v=sum/2时,遍历每个F[j][sum/2],并得到其中的最大值max{F[j][sum/2]}所对应的j就可以了。(需要把程序第二层循环中的j改成从min(i, 2n)开始?)

对于解法三,只要采用两重循环,for (v=sum/2; v>=1; v--) {for (j=1; j<=2n; j++) if (isOK[j][v]) break;},即可以得到使元素和最接近sum/2的某个子数组元素个数j。(同样需要把程序第二层循环中的j改成从min(i, 2n)开始?)


再引申一个问题:对于一个n个数的正整数数组,能否找到一个子数组,使其元素的和为某个数S?

解法四(正确性有待验证):

设F[i][v]表示前i个数中,是否能找到任意个数,和为v,则

(1) F[i][v] = F[i-1][v] || F[i-1][v-A[i]],v>=A[i]

(2) F[i][v] = F[i-1][v],v<A[i]

for (i = 1; i <= n; i++)
	for (v = 1; v <= S; v++)
	{	
		F[i][v] = F[i-1][v];
		if (v >= A[i] && F[i-1][v-A[i]]) F[i][v] = true;
	}

同样,由于F[i][]只与F[i-1][]有关,而F[i][v]只与F[i-1][v*(v*<=v)]有关,设F[v]表示当前(元素个数为i时)是否能找到任意个数和为v

for (i = 1; i <= n; i++)
	for (v = S; v >= 1; v--)
	{	
		if (v >= A[i] && F[v-A[i]]) F[v] = true;
	}

后记:

这个问题与0-1背包有着很大的相似性,有待进一步探究……



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值