文獻[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