问题

给定整数\(A_1, A_2, ..., A_N,\)求\(\sum_{k=i}^{j}A_k\)的最大值,其中\(A_i\)有可能是负数。这里把使得\(\sum_{k=i}^{j}A_k\)最大的序列称为最大连续子序列。

求解的算法

方法一

暴力枚举出所有可能的子序列的起点和终点,并求出对应子序列的和,从中选出最大值。算法使用了三层循环,一层用来枚举起点,一层用来枚举终点,还有一层用来求和。每一层循环的最大迭代次数都是n,故算法的时间复杂度为\(O(n \times n \times n) = O(n^3)\)。

    /**
     * triple loop solution, time complexity is cubic
     */
    public static int maxSubSum1(int[] array) {
        int maxSum = Integer.MIN_VALUE;

        for (int i = 0; i < array.length; i++) {
            for (int j = i; j < array.length; j++) {
                int thisSum = 0;
                for (int k = i; k <= j; k++) {
                    thisSum += array[k];
                }

                if (thisSum > maxSum) {
                    maxSum = thisSum;
                }
            }
        }

        return maxSum;
    }

方法二

方法二是方法一的改进。因为\(\sum_{k=i}^{j} = A_j + \sum_{k=i}^{j-1}\),所以方法一的三层循环可以改为两层循环:第一层循环枚举所有可能的连续子序列起点;第二层循环在起点到数组末尾的区间内从前往后累加,这相当于不断将终点延后一个位置直至数组末尾。累加过程中的最大值即是所求的最大连续子序列之和。这两层循环的时间复杂度是\(O(n \times n) = O(n^2)\)。

  /**
     * double loop solution, time complexity is quadratic
     */
    public static int maxSubSum2(int[] array) {
        int maxSum = Integer.MIN_VALUE;
        for (int i = 0; i < array.length; i++) {
            int thisSum = 0;

            for (int j = i; j < array.length; j++) {
                thisSum += array[j];
                if (thisSum > maxSum) {
                    maxSum = thisSum;
                }
            }
        }
        return maxSum;
    }

方法三

方法三是分而治之的递归算法。首先将序列从中间分割成两部分,那么最大连续子序列可能的位置有三种:

  1. 完全位于左半部分
  2. 完全位于右半部分
  3. 横跨左右两部分

分别计算出三种位置中的最大连续子序列,它们中序列和最大的那一个就是所求的结果。

对于第一和第二种位置,分别对左右两半部分进行递归调用即可;对于第三种位置,从中间开始向左右延伸,分别计算出与中间接壤的左右最大连续子序列,将这两个子序列加起来就是第三种位置的最大连续子序列。

令\(T(N)\)为求解规模为N的最大连续子序列问题时所花费的时间。在基准情况下,即N=1时,求解时间是常数的,因此我们记\(T(1)=1\)。当\(N>1\)时,算法会产生两个耗时\(T(N/2)\)的递归调用,而计算出第三种位置的最大连续子序列可能需要扫描整个数组,故其时间复杂度为\(O(N)\)。综上所述,我们有方程组 \(\begin{aligned} T(1) = 1 \\ T(N) = 2T(N/2) + O(N) \\ \end{aligned}\)

在大O分析中,我们可以将O(N)替换为N,得到\(T(N) = 2T(N/2) + N\)。假设\(N=2^k\),结合\(T(1) = 1\)我们可以得到并观察出 \(\begin{aligned} T(2) = 2*1 + 2 = 4 = 2 * (log2 + 1) \\ T(4) = 2*4 + 4 = 12 = 4 * (log4 + 1) \\ T(8) = 2*12 + 8 = 32 = 8 * (log8 + 1) \\ \end{aligned}\) 通过数学归纳法,我们可以求出通项公式\(T(N) = N * (k+1) = N*log(N) + N = O(N*log(N))\),也就是说方法三的时间复杂度为\(O(N*log(N))\)。

 /**
     * recursive divide and conquer solution, time complexity is n*log(n)
     */
    private static int maxSubSumRecursive(int[] array, int left, int right) {
        if (left == right) {
            return array[left];
        }

        int maxSum = Integer.MIN_VALUE;

        int middle = (left + right) / 2;
        int leftMaxSubSum = maxSubSumRecursive(array, left, middle);
        int rightMaxSubSum = maxSubSumRecursive(array, middle + 1, right);

        int leftBorderMaxSubSum = Integer.MIN_VALUE;
        int tempSum = 0;
        for (int i = middle; i >= left; i--) {
            tempSum += array[i];
            if (tempSum > leftBorderMaxSubSum) {
                leftBorderMaxSubSum = tempSum;
            }
        }

        int rightBorderMaxSubSum = Integer.MIN_VALUE;
        tempSum = 0;
        for (int i = middle + 1; i <= right; i++) {
            tempSum += array[i];
            if (tempSum > rightBorderMaxSubSum) {
                rightBorderMaxSubSum = tempSum;
            }
        }

        maxSum = Math.max(Math.max(leftMaxSubSum, rightMaxSubSum), leftBorderMaxSubSum + rightBorderMaxSubSum);
        return maxSum;
    }
    
    /**
     * driver function of maxSubRecursive
     */
    public static int maxSubSum3(int[] array) {
        return maxSubSumRecursive(array, 0, array.length - 1);
    }

方法四

方法四在扫描数组的同时进行累加。当累加结果为负数时,把累加值重置为0。这相当于抛弃前面的序列,重新进行累加。在扫描累加的过程中维持一个最大累加值的记录,扫描结束后的最大累加值就是所求的结果。

重置为0的操作不会使我们错过最优解,这是因为负数对于后续的求解没有作用,加上一个负数的连续子序列必然不是最大的。方法四把所有大于0的连续子序列都考虑进来了,因此可以保证最优解不会被遗漏。

PS:当数组中的所有元素均为负数时,算法会返回一个最小的负数元素。

方法四只扫描一遍数组,其时间复杂度仅为\(O(n)\)。

    /**
     * linear time solution
     */
    public static int maxSubSum4(int[] array) {
        int maxSum = Integer.MIN_VALUE;

        int thisSum = 0;
        for (int i = 0; i < array.length; i++) {
            thisSum += array[i];

            if (thisSum > maxSum) {
                maxSum = thisSum;
            }

            if (thisSum < 0) {
                thisSum = 0;
            }
        }

        return maxSum;
    }