最大子段和问题【思路及实现】

问题重述

最大子段和问题,又称最大子序列和问题。问题描述如下:

给定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)。

 

以上四种思路都能解决最大子段和问题,如有缺漏,欢迎大家补充。

你可能感兴趣的:(算法&数据结构)