最大子序列和问题

最大子序列和是指,给定一组序列,如 [1,-3,2,4,5],求子序列之和的最大值,对于该序列来说,最大子序列之和为 2 + 4 + 5 = 11。

这里的子序列要求是连续的,因此也可以称其为连续子数组最大和。

有几种不同的方法求解最大子序列和问题,但它们的复杂度相差甚远,尤其在面对大量数据的时候。实际上,效率最高的算法非常简短,只需要几行代码,最主要的是理解它的思想。


基本算法

这是一种比较容易想到的算法,也是一种比较慢的算法(当然还有比它更慢的算法,在此不再赘述)。算法的主要思想是求出所有的连续子序列的和,然后取最大值即可。

注意:对于数组全为负的情况,返回0

示例如下:

public static int base(int[] array) {
    int max = 0;
    for (int i = 0; i < array.length; i++) {
        int temp = 0;
        for (int j = i; j < array.length; j++) {
            temp += array[j];
            if (max < temp)
                max = temp;
        }
    }
    return max;
}

可以看到,时间复杂度为 O(n2)。

该问题还可以通过分治法来求解,最大连续子序列和要么出现在数组左半部分,要么出现在数组右半部分,要么横跨左右两半部分。因此求出这三种情况下的最大值就可以得到最大连续子序列和。

这种方法的时间复杂度为 O(nlgn),在此不再赘述。下面介绍一种更好的方法。


动态规划算法

该算法由 Jay Kadane 于1984年最先提出,运用了动态规划的思想,因此也称该方法为 Kadane 算法。

算法的时间复杂度为 O(n),能够在线性时间解决问题。虽然代码很简短,但还是需要好好理解这个算法的思想。

注意:对于数组全为负的情况,返回0

代码如下:

public static int get(int[] array) {
    int max = 0,temp = 0;
    for (int i = 0; i < array.length; i++) {
        temp += array[i];
        if (temp < 0)
            temp = 0;
        if (max < temp)
            max = temp;
    }
    return max;
}

以数组 [1,-3,2,4,5] 为例,用图形加以说明:

最大子序列和问题
下面是完整的程序示例:
public class MaxSequence {

    public static int getMax(int[] array) {
        int max = 0,temp = 0;
        for (int i = 0; i < array.length; i++) {
            temp += array[i];
            if (temp < 0)
                temp = 0;
            if ( max < temp)
                max = temp;
        }
        return max;
    }

    public static void main(String[] args) {
        int[] array1 = {-1,-3,-2,-4,-5};
        int[] array2 = {1,-3,2,4,5};
        System.out.println(getMax(array1));
        System.out.println(getMax(array2));
    }
}

输出结果:

0
11

循环列表中的最大子序列和问题

循环列表是指序列的首尾相连,例如对于 [1,-3,2,4,5]来说,可以用环形表示:
最大子序列和问题

如何求它的最大子序列和呢?

这个问题的解可以分为两种情况:

  1. 最大子序列没有跨越 array[n-1] 到 array[0] (原问题)
  2. 最大子序列跨越 array[n-1] 到 array[0]

当然在知道答案之前,我们并不知道解到底属于哪一种情况,因此可以将两种情况下的解都求出来,然后取其中的最大值就可以了。

对于第一种情况,按照之前的算法已经可以顺利求解出来;对于第二种情况,我们不妨换个思路,如果最大子序列跨越了 array[n-1] 到 array[0],那么最小子序列肯定不会出现跨越 array[n-1] 到 array[0] 的情况,如下图所示:

最大子序列和问题

所以,在允许数组跨界(首尾相邻)时,最大子数组的和为下面的最大值

max = { 原问题的最大子数组和,数组所有元素总值 - 最小子数组和 }

求解最小子数组和的方法与最大子数组和的方法正好相反。

示例如下:

public class MaxSequence {
    
    public static int getMax(int[] array) {
        int max = 0,temp = 0;
        for (int i = 0; i < array.length; i++) {
            temp += array[i];
            if (temp < 0)
                temp = 0;
            if ( max < temp)
                max = temp;
        }
        return max;
    }

    public static int getMin(int[] array) {
        int min = 0, temp = 0;
        for (int i = 0; i < array.length; i++) {
            temp += array[i];
            if (temp > 0)
                temp = 0;
            if (temp < min)
                min = temp;
        }
        return min;
    }

    public static int getLoopMax(int[] array) {
        int max1 = getMax(array);
        int min = getMin(array);
        int temp = 0;
        for (int i = 0; i < array.length; i++) {
            temp += array[i];
        }
        return Math.max(max1, temp - min);
    }

    public static void main(String[] args) {
        int[] array = {1,-3,2,4,5};
        System.out.println("原数组:" + Arrays.toString(array));
        System.out.println("最大子数组和(非循环):" + getMax(array));
        System.out.println("最小子数组和(非循环):" + getMin(array));
        System.out.println("最大子数组和(循环):" + getLoopMax(array));
    }
}

输出结果:

原数组:[1, -3, 2, 4, 5]
最大子数组和(非循环)11
最小子数组和(非循环)-3
最大子数组和(循环)12

参考链接: