将数组分区为尽可能多的连续部分

时间:2013-03-06 03:44:57

标签: algorithm

给定正整数的数组 A ,在第一部分的总和> =的总和下,找到连续部分的最大数量第二部分,第二部分的总和> =比第三部分的总和,依此类推。

我知道需要动态编程。我想到了计算所有可能分区的蛮力,然后将这个算法转换为memoized版本。

另一个想法是向后遍历数组,从最后一个元素开始作为一个分区,然后添加元素,直到它们的总和> =而不是下一个部分的总和。

非常欢迎任何建议。

3 个答案:

答案 0 :(得分:2)

这是一个有趣的问题。它类似于原始分区问题,除了这里分区的总和需要以单调递增的顺序。动态规划递归关系如下:

numP[i] = max {numP[i-j] + 1} for those values of j (1<=j<=i) such that sum(A[i-j,i-j+1,...,i] >= lastSum[i-j])
        = 1 for i = 1

lastSum[i] = the sum of the last partition in numP[i] solution.
           = A[0] for i = 1;

lastI[i] = starting index of the last partition in numP[i] solution; // this is needed to obtain the partitions in the solution

此处numP[i]表示可以使用数组的第一个i元素(未归零的数组)获取的最大分区数。我们递归地尝试在考虑数组的ith元素的情况下找到所有可能的解决方案,并输出获得的最大分区数。 j表示最后一个分区开始的索引。上面已经定义了lastSum[i]lastI[i]

这是动态编程解决方案的java实现。

void getMaxPartitions (int[] A) {

   int len = A.length;
   int[] numP = new int[len+1];
   int[] lastSum = new int[len+1];
   int[] lastI = new int[len+1];

   numP[1] = 1;
   lastSum[1] = A[0];
   lastI[1] = 0;
   for (int i = 2; i <= len; i++) {
     int maxIndex = 0;
     int maxPs = 0;
     int maxSum = 0;
     for(int j = 1; j <= i; j++) {
       int sum = 0;
       for(int k = i-j; k < i; k++) {
         sum += A[k];
       }
       if(sum >= lastSum[i-j]) {
         if(maxPs < numP[i-j] + 1) {
           maxPs = numP[i-j]+1;
           maxSum = sum;
           maxIndex = i-j;
         }
       }
     }
     numP[i] = maxPs;
     lastSum[i] = maxSum;
     lastI[i] = maxIndex;
   }
   System.out.println("max partitions = " + numP[len]);
   int i = len;
   while (i > 0) {
     System.out.println(lastSum[i]);
     i = lastI[i];
   }
}

对程序进行以下输入测试,结果如下:

(1) {3,4,7,1,5,4,11}     max partitions = 5 {3,4,8,9,11}
(2) {1,2,3,4,5,6,7,11}   max partitions = 8 {1,2,3,4,5,6,7,11}
(3) {1,1,1,1,1,1,1,1,1}  max partitions = 9 {1,1,1,1,1,1,1,1,1}
(4) {9,8,7,6,5,4,3,2,1}  max partitions = 3 {9,15,21}
(5) {40,8,7,6,5,4,3,2,1} max partitions = 1 {76}

答案 1 :(得分:1)

问题中建议的贪婪算法不起作用。您可能认为列表的最后一个元素确实应该是它自己的分区,但这是一个反例:

[16,15,1,5,7]

如果您将7作为一个分区,则最终会得到2个分区。但是16,15,(1 + 5 + 7)是更好的解决方案。

我建议你看看Integer/Discrete Programming via Branch and Bound

从最后一项开始,您的分支是您选择分区的不同选择(n代表最后一项),您可以通过考虑&gt; =规则检查部分解决方案的可行性,并且您的目标函数是最大数量分区。一个好的边界函数是假设所有剩余的项都是大小为1的分区。

我的另一个建议是在颠倒列表之后重新解释这个问题,它会更直观。反转列表并将规则更改为&lt; =并从头开始而不是结束。

答案 2 :(得分:0)

我最初想要一个贪婪的算法,但实际上需要在这里实现动态算法。

从最后开始似乎更直观。主要思想是,对于递归中的给定步骤,我们有一组已建立的分区,一个正在进行的分区,以及序列的其余部分。然后,算法必须选择最合适的解决方案,将下一个(或者前一个元素,如果我们在下面的代码中执行的操作)添加到当前分区,完成分区,或者在此处终止当前分区并完成分区(如果适用:当前分区可能未达到在此步骤终止的最小总和)。

我们必须跟踪分区p,当前位置i,当前部分的累计总和s以及之前的总和s'

这是OCaml中可能的实现(代码未经测试):

let  partition_by_growing_sums a =
    let n = Array.length a in
    let rec pbgs p s s' i =
       if i < 0 then  1 + List.length l, l::p
       else let x = a.(i) in
       if x+s >= s' then
         let n1, l1 = pbgs p (x+s) s' (pred i)
         and n2, l2 = pbgs (i::p) 0 (x+s) (pred i) in
         if n1 > n2 then n1, l1 else n2, l2
       else pbgs p (x+s) s' (pred i)
    in pbgs [] 0 0 (pred n)

对于memoizing,你需要跟踪从每个位置向前(或向后)完成的最佳分区,但是这个分区取决于该位置之后(或之前)分区所达到的总和,因此任何记忆的计算只会对于此位置导致相同状态的分区非常有用。我不确定这会非常有效。