文献[1][2][3]都是相同的解法,其中[4]的评论中有完整的解法,但不是很好理解,直到写这个文章时,还是不太理解。
文献[5]dlyme的回帖给了一个很好的思路,他的原文如下:
假设S=(a[1]+a[2]+...+a[n]+a[n+1]+...+a[2n])/2,
那么这是个和0-1背包类似的动态规划问题,区别之处就是取的个数受到限制,必须得取足n个元素。
用dp(i,j,c)来表示从前i个元素中取j个、且这j个元素之和不超过c的最佳(大)方案,在这里i>=j,c<=S
状态转移方程:
dp(i,j,c)=max{dp(i-1,j-1,c-a[i]),dp(i-1,j,c)}
dp(2n,n,S)就是题目的解。
整体复杂度是O(S*n^2)
如dlyme所分析,这个问题可以转换成一个二维背包问题,关于二维背包问题见文献[6]。
有一个问题,开始不太容易想清楚,就是如果累加的和大于sum/2的请如何处理?这个问题可以这样看,如果我们选出<=sum/2最接近的一组数,那么剩下的那组数就是>=sum/2最接近sum/2的一组数了,因此只要考虑<=sum/2的情况就可以了,因为剩下的数刚好就是>=sum/2且最接近sum/2的一组数。
问题就转换为,从2n个数中,选取n个数装在背包中,使得结果最大,而背包的容量就是sum/2。这就是一个二维背包问题了,必须测试2n中的每一个数,分为选取这个数和不选取这个数的两种情况,如果选取该数得到的结果dp[i-1][j-1][c-a[i]] + a[i] 大于不选取该数的情况,就选取该数,否则不选取该数。如果选取了该数,那么待选的总数-1(i-1),需要选择的数量也-1(j-1),背包剩余容量也要-a[i](c- a[i]),如果不选则该数,那么待选的总数-1(i -1),其他保持不变。
这样计算出最后的dp(2n,n,sum/2)就是选择的最后结果,如果选择了某个数,则记录下在总数、还有多少未选以及背包剩余容量的情况下,选择了该数,以便于回溯输出,我们使用select三维数组来记录相关的信息。
具体测试程序如下:
#include <stdio.h>
void ArrayPartition(int array[], int size) {
int array_size = size;
int sum = 0;
for (int i = 0; i < array_size; ++i) {
sum += array[i];
}
printf("%d \n", sum / 2);
int*** cost = new int**[array_size + 1];
for (int i = 0; i < array_size + 1; ++i) {
cost[i] = new int*[array_size / 2 + 1];
for (int j = 0; j < array_size / 2 + 1; ++j) {
cost[i][j] = new int[sum / 2 + 1];
}
}
int*** select = new int**[array_size + 1];
for (int i = 0; i < array_size + 1; ++i) {
select[i] = new int*[array_size / 2 + 1];
for (int j = 0; j < array_size / 2 + 1; ++j) {
select[i][j] = new int[sum / 2 + 1];
}
}
for (int i = 0; i < array_size + 1; ++i) {
for (int j = 1; j <= array_size / 2; ++j) {
for (int v = 1; v <= sum / 2; ++v) {
cost[i][j][v] = 0;
select[i][j][v] = 0;
}
}
}
for (int i = 0; i < array_size; ++i) {
for (int j = 1; j <= array_size / 2; ++j) {
for (int v = 1; v <= sum / 2; ++v) {
if (v >= array[i]) {
if (cost[i][j-1][v-array[i]] + array[i] > cost[i][j][v]) {
cost[i+1][j][v] = cost[i][j-1][v-array[i]] + array[i];
select[i+1][j][v] = 1;
} else {
cost[i+1][j][v] = cost[i][j][v];
}
}
}
}
}
int j = array_size / 2 ;
int v = sum / 2;
for (int i = array_size; i > 0; --i) {
if (select[i][j][v] == 1) {
printf("%d ", array[i - 1]);
j -= 1;
v -= array[i - 1];
}
}
for (int i = 0; i < array_size; ++i) {
for (int j = 1; j <= array_size / 2; ++j) {
delete[] cost[i][j];
delete[] select[i][j];
}
delete[] cost[i];
delete[] select[i];
}
delete[] cost;
delete[] select;
}
int main(int argc, char** argv) {
int array[] = {1, 2, 4, 5, 6, 7, 8};
ArrayPartition(array, sizeof(array) / sizeof(int));
}
其实j的循环截止条件是min[i,array_size/2],因为 j > i是没有意义的,因为从i个元素中无法选择大于i个元素,但多循环几次也暂不做修改了。另外,很好的理解经典背包问题,会很有利于这个问题的理解,经典背包问题见文献[7]
参考文献:
[1]编程之美2.18
[3]http://www.4ucode.com/Study/Topic/670996
[5]http://topic.csdn.net/u/20080921/12/448b7e06-0a87-4ede-882f-01f441ae7353.html