给定一个正整数数组A ,在第一个部分的和 >= 比第二个部分的和,第二个部分的和 >= 比第三个部分的和的条件下,找到最大连续部分的数量, 等等。
我知道需要动态编程。我想到了计算所有可能的分区然后将该算法转换为记忆版本的蛮力。
另一个想法是向后遍历数组,从最后一个元素作为一个分区开始,然后将元素相加,直到它们的总和 >= 比下一部分的总和。
任何建议都非常受欢迎。
给定一个正整数数组A ,在第一个部分的和 >= 比第二个部分的和,第二个部分的和 >= 比第三个部分的和的条件下,找到最大连续部分的数量, 等等。
我知道需要动态编程。我想到了计算所有可能的分区然后将该算法转换为记忆版本的蛮力。
另一个想法是向后遍历数组,从最后一个元素作为一个分区开始,然后将元素相加,直到它们的总和 >= 比下一部分的总和。
任何建议都非常受欢迎。
这是一个有趣的问题。它类似于原始的分区问题,只是这里分区的总和需要按单调递增的顺序排列。动态规划递推关系如下:
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}
问题中建议的贪心算法不起作用。您可能认为列表的最后一个元素确实应该是它自己的一个分区,但这里有一个反例:
[16,15,1,5,7]
如果你有 7 个作为一个分区,你最终会得到 2 个分区。但是 16,15,(1+5+7) 是更好的解决方案。
我建议你看看Integer/Discrete Programming via Branch and Bound
您从最后一项开始,您的分支是您选择分区的不同选择(最后一项为 n),您通过考虑 >= 规则检查部分解决方案的可行性,并且您的目标函数是最大化分区数。一个好的边界函数是假设所有剩余的项目都是大小为 1 的分区。
我的另一个建议是在颠倒列表后重新表述问题,这样会更直观。反转列表并将规则更改为 <= 并从开头而不是结尾开始。
我最初打算使用一种贪心算法,但确实需要在这里实现一个动态算法。
从头开始看起来更直观。主要思想是,对于递归中的给定步骤,我们有一组已建立的分区、一个正在进行的分区以及序列的其余部分。然后,算法必须在向当前分区添加下一个(或前一个元素,如果向后移动,这是我们在下面的代码中所做的)和完成分区之间选择最合适的解决方案,或者在此处终止当前分区并完成分区(如果适用:当前分区可能未达到在此步骤终止的最小总和)。
我们必须跟踪分区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)
对于记忆,您需要跟踪从每个位置向前(或向后)完成的最佳分区,但此分区取决于该位置之后(或之前)的分区所达到的总和,因此任何记忆的计算只能用于导致该位置相同状态的分区。我不确定这会非常有效。