最大子段和问题,又称最大子序列和问题。问题描述如下:
给定n个整数(可能为负数)组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的子段和的最大值。当所给的整数均为负数时定义子段和为0,依此定义,所求的最优值为: Max{0,a[i]+a[i+1]+…+a[j]},1<=i<=j<=n。
例如,当(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])=(4, -3, 5, -2, -1, 2, 6, -2)时,最大子段和为11。
首先给出实现代码的方法接口。
/**
* Linear-time maximum contiguous subsequence sum algorithm.
*
* @param a
* Array a
* @return
* Maximum subsegment sum of array a
*/
public static int maxSubSum(int[] a){
// Method body
}
思路一
对于该问题,最容易的想法就是利用穷举。将 i = [ 0 to a.length - 1 ] 依次作为开始索引,从 i 开始,不断进行偏移,得到 j = [ i to a.legth - 1 ],然后求出索引从 i 到 j 之间所有元素的之和,即当前的最大子段和。若当前的最大子段和大于最大子段和,那么就将当前的子段和作为最大子段和。
穷举结束后的最大子段和就一定整个数组的最大子段和。下面给出该思路的实现代码。
/**
* Cubic maximum contiguous subsequence sum algorithm.
* */
public static int maxSubSum1(int[] a) {
int maxSum = 0;
// start position
for (int i = 0; i < a.length; i++) {
// end position
for (int j = i; j < a.length; j++) {
int thisSum = 0;
for (int k = i; k <= j; k++) {
thisSum += a[k];
}
if (thisSum > maxSum) {
maxSum = thisSum;
}
}
}
return maxSum;
}
该算法实现的时间复杂度为O(N^3)。
思路二
思路二与思路一的具体实现思路都是相同的。但观察思路一的实现,不难发现该实现相对是低效的。最内层的循环每次都会对索引 i 到 j 的元素进行一次加和。实际上,完全可以撤去最内层的for循环。thisSum每次只加上当前的 a[ j ],就能保证thisSum中始终保存的是索引 i 到 j 的元素的一个和。其实现代码如下。
/**
* Quadratic maximum contiguous subsequence sum algorithm.
* */
public static int maxSubSum2(int[] a) {
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
int thisSum = 0;
for (int j = i; j < a.length; j++) {
thisSum += a[j];
if (thisSum > maxSum) {
maxSum = thisSum;
}
}
}
return maxSum;
}
由于消去了最内层循环。所以复杂度降到了O(N^2)。
思路三
以上两种思路都是基于穷举,复杂度都相对较高。对于这个问题,还可以采用分治法来解决。但不同于等分的分治算法。解决该问题的分治思路是非对称的分治。
利用分治算法不难将数组分为两个部分,然后继续划分并解决。但实际上,最大子段和可以出现在三个地方——数组左子段,数组右子段,或者跨越左右子段出现在数组的中间位置。
前两种情况利用分治就能很好的解决,但是对于出现在中间的这种情况。必须进行特殊处理。简单的说,就是从中间位置开始,分别向左右两边取得最大的和,最终的和就是最大的中间子段和。
最大左子段和,最大右子段和,最大中间子段和中的最大值,就是整个数组的最大子段和。下面给出实现代码。
/**
* Driver for divide-and-conquer maximum contiguous subsequence sum algorithm.
* */
public static int maxSubSum3(int[] a) {
return maxSumRec(a, 0, a.length - 1);
}
/**
* Recursive maximum contiguous subsequence sum algorithm.
* Finds maximum sum in subarray spanning a[left..right].
* */
private static int maxSumRec(int[] a, int left, int right) {
// Base case & Recursive Export
if (left == right) {
if (a[left] > 0) {
return a[left];
} else {
return 0;
}
}
int center = (left + right) / 2;
// Maximum left subsegment sum
int maxLeftSum = maxSumRec(a, left, center);
// Maximum right subsegment sum
int maxRightSum = maxSumRec(a, center + 1, right);
// Maximum left border sum
int maxLeftBorderSum = 0, leftBorderSum = 0;
for (int i = center; i >= left; i--) {
leftBorderSum += a[i];
if (leftBorderSum > maxLeftBorderSum) {
maxLeftBorderSum = leftBorderSum;
}
}
// Maximum right border sum
int maxRightBorderSum = 0, rightBorderSum = 0;
for (int i = center + 1; i <= right; i++) {
rightBorderSum += a[i];
if (rightBorderSum > maxRightBorderSum) {
maxRightBorderSum = rightBorderSum;
}
}
// Maximum middle subsegment sum
int maxMidSum = maxLeftBorderSum + maxRightBorderSum;
// max(maxLeftSum, maxMidSum, maxRightSum);
if (maxLeftSum > maxMidSum && maxLeftSum > maxRightSum) {
return maxLeftSum;
} else if (maxMidSum > maxRightSum) {
return maxMidSum;
} else {
return maxRightSum;
}
}
该算法的时间复杂度为O(NlogN)。
思路四
思路三的实现在时间复杂度上已经较为良好,但是实际上解决该问题仍有更简单的实现思路。该思路复杂度能达到O(N)。
首先提出一个假设:任何负的子序列都不可能是最优子序列(即和最大的子序列)的前缀。
举个例子,进行一下反证:
假如现在有个子序列S1 = { -1, -2, -3, 1, 2, 3, 4, 5 }是某个序列S的最优子序列。其前缀子序列P = { -1, -2, -3 }为负。
那么去掉该前缀子序列P,可以得到一个更优的子序列S2 = { 1, 2, 3, 4, 5 }。既然S1是S的子序列,则S2也必然是S的子序列。那么就说明S1不是S的最优子序列。与假设冲突。
根据以上反证过程,证实了之前假设的正确性。通过该假设。我们便能通过一重for循环解决该问题。简单说下思路。
利用for循环遍历整个数组a,每次利用一个thisSum加上当前元素,得到当前的子序列和thisSum。若thisSum为负,那么则说明该序列必然不是最优子序列的前缀,将thisSum置为0。否则若thisSum大于最大子序列和maxSum,那么将thisSum赋值给maxSum。实现代码如下。
/**
* Linear-time maximum contiguous subsequence sum algorithm.
* */
public static int maxSubSum4(int[] a){
int maxSum = 0, thisSum = 0;
for(int i = 0; i < a.length; i++){
thisSum += a[i];
if(thisSum > maxSum){
maxSum = thisSum;
}else if(thisSum < 0){
thisSum = 0;
}
}
return maxSum;
}
这种思路的实现时间复杂度为O(N)。
以上四种思路都能解决最大子段和问题,如有缺漏,欢迎大家补充。