问题:有一个无序、元素个数为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背包有着很大的相似性,有待进一步探究……