最大子数组问题

来源:互联网 发布:网络病毒的加密技术 编辑:程序博客网 时间:2024/06/05 14:27

1.问题描述
问题:一个有N个整数元素的一维数组(A[0],A[1],A[2],…A[n-1]),这个数组中子数组之和的最大值是多少?
该子数组是连续的。例如 数组:[1,-2,3,5,-3,2]返回8; 数组:[0,-2,3,5,-1,2]返回9。

网上有称之为最大子序列和,亦有称连续子数组最大和。个人觉得叫最大子序列和不太妥,数学上讲,子序列不一定要求连续,而这里我们的题目必然要求是连续的,如果不连续而求子序列最大和很显然就无意义了,这也是为啥又称连续子数组最大和。不过,莫要在意细节。

这题是很经典的一道面试题,也有各种解法,从算法分析上,时间复杂度也有很大差别,下面我就给出三种不同的解法。

2.解法一:暴力枚举法
此种方法最简单,我想应该也是每个人拿到题目想到的第一种解法了,学过一点编程的人都应该能编出此类程序。

记sum[i..j]为数组中第i个元素到第j个元素的和(其中0<=i

int maxSubArray(int *A,int n) {    int maxium = -INF; //保存最大子数组之和    for i=0 to n-1 do        sum = 0; //sum记录第i到j的元素之和        for j=i to n-1 do            sum += A[j];        if sum>maxium do //更新最大值            maxium = sum;    return maxium;}

此种方法的时间复杂度为O(n2),显然不是一种很好的办法,也不是公司面试希望你写出这样的程序的。

3.解法二:分支界定
这里再介绍一种更高效的算法,时间复杂度为O(nlogn)。这是个分治的思想,解决复杂问题我们经常使用的一种思维方法——分而治之。

而对于此题,我们把数组A[1..n]分成两个相等大小的块:A[1..n/2]和A[n/2+1..n],最大的子数组只可能出现在三种情况:
A[1..n]的最大子数组和A[1..n/2]最大子数组相同;
A[1..n]的最大子数组和A[n/2+1..n]最大子数组相同;
A[1..n]的最大子数组跨过A[1..n/2]和A[n/2+1..n]
前两种情况的求法和整体的求法是一样的,因此递归求得。

第三种,我们可以采取的方法也比较简单,沿着第n/2向左搜索,直到左边界,找到最大的和maxleft,以及沿着第n/2+1向右搜索找到最大和maxright,那么总的最大和就是maxleft+maxright。
而数组A的最大子数组和就是这三种情况中最大的一个。
伪代码如下:

int maxSubArray(int *A,int l,int r) {    if l<r do        mid = (l+r)/2;        ml = maxSubArray(A,l,mid); //分治        mr = maxSubArray(A,mid+1,r);        for i=mid downto l do            search maxleft;        for i=mid+1 to r do            search maxright;        return max(ml,mr,maxleft+maxright); //归并        then //递归出口            return A[l];}

4.解法三:动态规划(DP)
我们考虑最后一个元素arr[n-1]与最大子数组的关系,有如下三种情况:
(1)arr[n-1]单独构成最大子数组
(2)最大子数组以arr[n-1]结尾
(3)最大子数组跟arr[n-1]没关系,最大子数组在arr[0-n-2]范围内,转为考虑元素arr[n-2]

从上面我们可以看出,问题分解成了三个子问题,最大子数组就是这三个子问题的最大值,现假设:
(1) 以arr[n-1]为结尾的最大子数组和为End[n-1]
(2) 在[0-(n-1)]范围内的最大子数组和为All[n-1]
如果最大子数组跟最后一个元素无关,即最大和为All[n-2](存在范围为[0-n-2]),则解All[n-1]为三种情况的最大值,即All[n-1] = max{ arr[n-1],End[n-1],All[n-2] }。从后向前考虑,初始化的情况分别为arr[0],以arr[0]结尾,即End[0] = arr[0],最大和范围在[0,0]之内,即All[0]=arr[0]。根据上面分析,给出状态方程:
All[i] = max{ arr[i],End[i-1]+arr[i],All[i-1] }
代码如下:

/* DP base version*/#define max(a,b) ( a > b ? a : b)int Maxsum_dp(int * arr, int size){    int End[30] = {-INF};    int All[30] = {-INF};    End[0] = All[0] = arr[0];    for(int i = 1; i < size; ++i)    {        End[i] = max(End[i-1]+arr[i],arr[i]);        All[i] = max(End[i],All[i-1]);    }    return All[size-1];}

上述代码在空间上是可以优化为O(1)的

/* DP base version*/#define max(a,b) ( a > b ? a : b)int Maxsum_dp(int * arr, int size){    nEnd = nAll = arr[0];    for(int i = 1; i < size; ++i)    {        nEnd  = max(nEnd +arr[i],arr[i]);        nAll = max(End[i],nAll);    }    return nAll ;}

下面说一下由DP而导出的另一种O(N)的实现方式,该方法直观明了,个人比较喜欢,所以后续问题的求解也是基于这种实现方式来的。
仔细看上面DP方案的代码,End[i] = max{arr[i],End[i-1]+arr[i]},如果End[i-1]<0,那么End[i]=arr[i],什么意思?End[i]表示以i元素为结尾的子数组和,如果某一位置使得它小于0了,那么就自当前的arr[i]从新开始,且End[i]最初是从arr[0]开始累加的,所以这可以启示我们:我们只需从头遍历数组元素,并累加求和,如果和小于0了就自当前元素从新开始,否则就一直累加,取其中的最大值便求得解。
到这里其实就可以了,在《编程之美》中,作者故意没有按照这种推导来实现(我猜的),而是在End[i-1]<0时,让End[i]=0,从而留出了一个问题(元素全是负数怎么办),其实如果按照上面的推导直接实现的话,就不存在这个问题了;
基于上面的推导,代码如下:

/* DP ultimate version */int Maxsum_ultimate(int * arr, int size){    int maxSum = -INF;    int sum = 0;    for(int i = 0; i < size; ++i)    {        if(sum < 0)        {            sum = arr[i];        }else        {            sum += arr[i];        }        if(sum > maxSum)        {            maxSum = sum;        }    }    return maxSum;}

其实上面的方法虽说是从DP推导出来的,但是写完发现也是很直观的方法,求最大和,那就一直累加呗,只要大于0,就说明当前的“和”可以继续增大,如果小于0了,说明“之前的最大和”已经不可能继续增大了,就从新开始,如此这样。

5.问题扩展:返回最大子数组始末位置
这个问题是《编程之美》2.14的扩展问题,返回始末位置还是比较容易的,我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便对应可能的结束下标,这个时候,只要顺便用本轮的起始和结束位置更新始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了。

C++代码如下:

#include<iostream>using namespace std;template<class T>class my_data_type{    T max_sum;    int start;    int end;public:    void max_sub_sum_dp(T *a, int length);    void my_type_printf(void);};template<class T>void my_data_type<T>::my_type_printf(void){    cout << "max_sum is " << max_sum << endl;    cout << "start is " << start << "   end is " << end << endl;}template<class T>void my_data_type<T>::max_sub_sum_dp(T *a, int length){    T sum=a[0];    start = 0;    end = 0;    max_sum = a[0];    int start_temp = 0;    for (int i = 1; i < length; i++)    {           if (sum < 0)        {            sum = a[i];            start_temp = i;        }        else        {            sum = sum + a[i];        }        if (sum > max_sum)        {            max_sum = sum;            start = start_temp;            end = i;        }    }}template<typename T>void printf_data(T *a, int length){    for (int i = 0; i < length; i++)    {        cout << a[i] << " ";    }    cout << endl;}int main(){    int a_int[6] = { 0, -2, 3, 5, -1, 2 };    int a1_int[6] = { -9, -2, -3, -5, -3 };    float a_float[6] = { 0.1, -2.2, 3.4, 5.6, -1.3, 2.8 };    float a1_float[5] = { -9.0, -2.1, -3.1, -5.2, -3.6 };    my_data_type<int> my_test_int;    my_test_int.max_sub_sum_dp(a_int, 6);    printf_data(a_int, 6);    my_test_int.my_type_printf();    my_test_int.max_sub_sum_dp(a1_int, 5);    printf_data(a1_int, 5);    my_test_int.my_type_printf();    my_data_type<float> my_test_float;    my_test_float.max_sub_sum_dp(a_float, 6);    printf_data(a_float, 6);    my_test_float.my_type_printf();    my_test_float.max_sub_sum_dp(a1_float, 5);    printf_data(a1_float, 5);    my_test_float.my_type_printf();}

测试结果:
测试结果

6.问题扩展:允许数组首尾相连
这个也是2.14的扩展问题,如果数组arr[0],…,arr[n-1]首尾相邻,也就是允许找到一段数字arr[i],…,arr[n-1],arr[0],…,a[j],使其和最大,该如何?

编程之美解法:这个问题的解可以分为两种情况:

1) 解没有跨越arr[n-1]到arr[0] (原问题)

2) 解跨越arr[n-1]到arr[0]

对于第一种情况按照之前的方式计算即可,对于第二种情况我们可以巧妙地 进行问题转换。我们找最大子数组的对偶问题——最小子数组,有了最小子数组的值,总值减去它不就可以了么?但是我又想,这个对偶问题只能处理这种跨界的特殊情况吗?答案是肯定的,如果最大子数组跨界,那么剩余的中间那段和就一定是最小的,而且和必然是负的;相反,如果最大子数组不跨界,那么总值减去最小子数组的值就不一定是最大子数组和了,例如例子[8,-10,60,3,-1,-6],最大子数组为[8 | 60,3,-1,-6],而最小子数组和为[-10],显然不能用总值减去最小值。
故,在允许数组跨界(首尾相邻)时,最大子数组的和为下面的最大值
Maxsum={ 原问题的最大子数组和;数组所有元素总值-最小子数组和 }。

1 0
原创粉丝点击