數組分割問題

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

[2]http://s.sousb.com/2011/04/06/%E7%BC%96%E7%A8%8B%E4%B9%8B%E7%BE%8E%E6%95%B0%E7%BB%84%E5%88%86%E5%89%B2/

[3]http://www.4ucode.com/Study/Topic/670996

[4]http://4develop.in/page/CPPLanguage/20111115_19_84687c82-97a2-42c0-9049-ccb305b0e6e3/%E7%BC%96%E7%A8%8B%E4%B9%8B%E7%BE%8E%E6%95%B0%E7%BB%84%E5%88%86%E5%89%B2.html

[5]http://topic.csdn.net/u/20080921/12/448b7e06-0a87-4ede-882f-01f441ae7353.html

[6]http://love-oriented.com/pack/P05.html

[7]http://blog.csdn.net/bertzhang/article/details/7262302

發佈了82 篇原創文章 · 獲贊 3 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章