数组分割问题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值