给定正整数的数组 A ,在第一部分的总和> =的总和下,找到连续部分的最大数量第二部分,第二部分的总和> =比第三部分的总和,依此类推。
我知道需要动态编程。我想到了计算所有可能分区的蛮力,然后将这个算法转换为memoized版本。
另一个想法是向后遍历数组,从最后一个元素开始作为一个分区,然后添加元素,直到它们的总和> =而不是下一个部分的总和。
非常欢迎任何建议。
答案 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,你需要跟踪从每个位置向前(或向后)完成的最佳分区,但是这个分区取决于该位置之后(或之前)分区所达到的总和,因此任何记忆的计算只会对于此位置导致相同状态的分区非常有用。我不确定这会非常有效。