首页 > 技术文章 > 数组的子数组之和的最大值

fickleness 2013-06-25 16:48 原文

        求子数组之和的最大值是一个很经典的问题。问题的描述如下:一个有N个整形数的一维数组(A[0], A[1], ... A[n-1]),这个数组有很多子数组,那么子数组之和的最大值是什么呢?

         这个问题的解答其实在《编程珠玑》一书有的。一共是4中方法:第一种是穷举法,计算所有可能子数组的和,时间复杂度为O(n3)。

 

第二种其实也是穷举法。代码如下:

[cpp] view plaincopy

  1. for(i = 0;i < n;i++)  
  2. {  
  3.  sum = 0;  
  4.  for(j = i;j < n;j++)  
  5.  {  
  6.   sum += A[j];  
  7.   if(sum > maxsum)  
  8.    maxsum = sum;  
  9.  }  

10. }  


 

         很明显复杂度为O(n2)。第三种方法是分治法,将数组元素均分成两部分,那么最大子数组和只有三种情况。在左边部分,右边部分,以及跨越了边界部分。这种方法是时间复杂度为O(nlogn)。不是最优的就不列代码了。

第四种是最优的,时间复杂度为O(n),利用了动态规划的思想。具体代码如下:

 

A[0]是首元素,

start[1]是包含a[1]的和最大的数组,

all[1]是可能包含也可能不包含a[1]的和最大的数组,

所以a[0]…..a[n-1]的解all[0]是max{a[0],   a[0]+start[1],  all[1]};

 

  1. int MaxSubSum1(int *A,int n)  
  2. {  
  3.     int start[n-1], all[n-1];  
  4.     all[n-1]= A[n-1];
  5.      start[n-1]=A[n-1];  
  6.     for(int i = n-2; i >= 0; i--)  
  7.     {  
  8.         start[i] = Max(A[i], start[i+1] + A[i]);  
  9.         all[i] = Max(all[i+1], start[i]);  
  10. 10.     }  
  11. 11.     return all[0];  

12. }  

这个代码有额外申请了两个数组,下面是改进的算法,避免开辟数组空间

 

 

分治算法的模型

 

[cpp] view plaincopy

13. int MaxSubSum1(int *A,int n)  

14. {  

  1. 15.     int start, all;  
  2. 16.     all = start = A[n-1];  
  3. 17.     for(int i = n-2; i >= 0; i--)  
  4. 18.     {  
  5. 19.         start = Max(A[i], start + A[i]);  
  6. 20.         all = Max(all, start);  
  7. 21.     }  
  8. 22.     return all;  

23. }  

还可以进行下一步的改进;

24. int MaxSubSum1(int *A,int n)  

25. {  

  1. 26.     int start, all;  
  2. 27.     all = start = A[n-1];  
  3. 28.     for(int i = n-2; i >= 0; i--)  
  4. 29.     {  
  5. 30.        if(start<0)
  6. 31.             start=0;
  7. 32.        start+=a[i];
  8. 33.        if(start>all)
  9. 34.            all=start;
  10. 35.         
  11. 36.  
  12. 37.     }  
  13. 38.     return all;  


 

        这种方法不论是空间和时间都已是最优的了,在《编程之美》中列举了改进的过程,最终的程序就是上面的这段代码。

        如果是二维数组呢,又当如何解答。《编程之美》中给出的解法是穷举矩形区域的所有可能的上下边界,再用一维的方法计算该上下边界的最大和。时间复杂度为 O(n2*m)。当然也可以先穷举矩形区域的所有可能的左右边界。本质上是一样的。下面给出了一种解法。这里用了一个辅助数组B,B[ i ][ j ]表示第 j 列元素的前 i  行元素的和。B[ i + 1 ][ j ]=A[ 0 ][ j ]+A[ 1 ][ j ]+...A[ i ][ j ]。B[ 0 ][ j ]做为哨兵,全部为0。

[cpp] view plaincopy

  1. int MaxSubSum2(int *A, int n, int m)    
  2. {    
  3.     int i, j, k;    
  4.     //初始化辅助数组    
  5.     int **B = new int*[n+1];    
  6.     for(i = 0; i <= n; i++)    
  7.         B[i] = new int[m];    
  8.     for(j = 0; j < m; j++)  //第0行做为哨兵    
  9.         B[0][j] = 0;    
  10. 10.     for(i = 0; i < n; i++)    
  11. 11.         for(j = 0; j < m; j++)    
  12. 12.             B[i+1][j] = B[i][j]+A[i*n+j];    
  13. 13.     //开始计算    
  14. 14.     int maxsum = 0x80000000;  //设为最小值    
  15. 15.     for(i = 1; i <= n; i++)    
  16. 16.     {    
  17. 17.         for(j = i; j <= n; j++)    
  18. 18.         {    
  19. 19.             int start, all;    
  20. 20.             start = all = (B[j][m-1]-B[i-1][m-1]);    
  21. 21.             for(k = m-2; k >= 0; k--)    
  22. 22.             {    
  23. 23.                 start = Max(B[j][k]-B[i-1][k], start + B[j][k]-B[i-1][k]);     
  24. 24.                 all = Max(all, start);    
  25. 25.             }    
  26. 26.             if(all > maxsum)    
  27. 27.                 maxsum = all;    
  28. 28.         }    
  29. 29.     }    
  30. 30.     for(i = 0; i <= n; i++) //释放辅助空间  
  31. 31.         delete [] B[i];  
  32. 32.     delete B;  
  33. 33.     return maxsum;    

34. }  

问题描述:

一个有N个整数元素的一维数组(A[0],A[1],...A(n-1),它包含很多子数组,求子数组之和的最大值,当数组元素全部为负的时候,有两种处理办法,第一种是返回0,第二种是返回数组中最大的负数。

解法1:

使用暴力法,假设最大的一段数组为A[i],...,A[j],则对i:=0~n-1 j:=i~n-1,遍历一遍,求出最大的Sum(i,j)即可

解法2:

使用分治法,数组(A[0],A[1],...A(n-1)分为长度相等的两段数组(A[0],...,A[n/2])以及(A[n/2+1],...,A[n-1]),分别求出这两段数组各自的最大子段和,则原数组(A[0],A[1],...A(n-1)的最大子段和分为3种情况
1)(A[0],A[1],...A(n-1)的最大子段和与(A[0],...,A[n/2])的相同
2)(A[0],A[1],...A(n-1)的最大子段和与(A[n/2+1],...,A[n-1])的相同
3)(A[0],A[1],...A(n-1)的最大子段和跨过(A[0],...,A[n/2])与(A[n/2+1],...,A[n-1])

解法3:
使用动态规划法,假设A[0],A[1],...A(n-1)的最大子段为A[i],...,A[j],则有以下3种情况
1)当0=i=j的时候,元素A[0]本身构成和最大的一段
2)当0=i<j的时候,和最大的一段以A[0]开头
3)当0<i时候,元素A[0]跟和最大的一段没有关系
则原始问题A[0],A[1],...A(n-1)的解All[0]=max{A[0],A[0]+Start[1],ALL[1]}

 

程序说明:

下面的程序中,针对数组全部为负的情况的处理(处理1:直接返回0,处理2:返回数组中最大的负数)定义了两种情况,分别对应于定义了RETURN_ZERO和RETURN_MAXMINUS这两种情况

 

    /*********************问题描述******************

一个有N个整数元素的一维数组(A[0],A[1],...A(n-1),
    它包含很多子数组,求子数组之和的最大值,当数组元
    素全部为负的时候,有两种处理办法,第一种是返回0,
    第二种是返回数组中最大的负数
    ************************************************/  
    #include<iostream>  
    using namespace std;  
      
      
    //定义了RETURN_ZERO说明处理全部为负数的数组时候返回0  
    #define RETURN_ZERO  
    #ifdef RETURN_ZERO  
    /*******************************解法一:蛮力法*****************************
    依次计算A[0],A[0..1],...A[0..n-1],A[1],A[1..2]...A[1..n-1],A[2],A[2..3],...
    A[2..n-1]...并返回最大的值maximum
    ****************************************************************************/  
    int MaxSum1(int *A,int length){  
        int maximum=0;          //子数组和最大值  
        int sum;                //子数组和  
        for(int i=0;i<length;i++){  
            sum=0;  
            for(int j=i;j<length;j++){  
                sum+=A[j];  
                if(sum>maximum)  
                    maximum=sum;  
            }  
        }  
        return maximum;  
    }  
    /*******************************解法二:分治法*******************************
    数组(A[0],A[1],...A(n-1)分为长度相等的两段数组(A[0],...,A[n/2])以及(A[n/2+1],
    ...,A[n-1]),分别求出这两段数组各自的最大子段和,则原数组(A[0],A[1],...A(n-1)
    的最大子段和分为3种情况:
    1)(A[0],A[1],...A(n-1)的最大子段和与(A[0],...,A[n/2])的相同
    2)(A[0],A[1],...A(n-1)的最大子段和与(A[n/2+1],...,A[n-1])的相同
    3)(A[0],A[1],...A(n-1)的最大子段和跨过(A[0],...,A[n/2])与(A[n/2+1],...,A[n-1])
    *****************************************************************************/  
    int MaxSum2(int *A,int left,int right){  
        if(left==right){  
            return max(A[left],0);  
        }  
        int middle=(left+right)/2;  
        //求(A[0],...A[n/2-1])中子数组包含A[n/2-1]的最大值  
        int lmaximum=0;  
        int lsum=0;  
        for(int i=middle;i>=left;i--){  
            lsum+=A[i];  
            if(lsum>lmaximum)  
                lmaximum=lsum;  
        }  
        //求(A[n/2],...,A[n-1])中子数组包含A[n/2]的最大值  
        int rmaximum=0;  
        int rsum=0;  
        for(int i=middle+1;i<=right;i++){  
            rsum+=A[i];  
            if(rsum>rmaximum)  
                rmaximum=rsum;  
        }  
        return max(lmaximum+rmaximum,max(MaxSum2(A,left,middle),MaxSum2(A,middle+1,right)));  
    }  
    /******************************解法三:动态规划**********************************
    假设A[0],A[1],...A(n-1)的最大子段为A[i],...,A[j],则有以下3种情况,
    1)当0=i=j的时候,元素A[0]本身构成和最大的一段
    2)当0=i<j的时候,和最大的一段以A[0]开头
    3)当0<i时候,元素A[0]跟和最大的一段没有关系
    则原始问题A[0],A[1],...A(n-1)的解All[0]=max{A[0],A[0]+Start[1],ALL[1]}
    *********************************************************************************/  
    //从尾到首动态规划(编程之美2.14的思想)  
    int MaxSum3(int *A,int length){  
        int nStart=0;  
        int nAll=0;  
        for(int i=length-1;i>=0;i--){  
            nStart=max(0,A[i]+nStart);  
            nAll=max(nStart,nAll);  
        }  
        return nAll;  
    }  
    //从首到尾动态规划(编程珠玑8.4思想)  
    int MaxSum3_v2(int *A,int length){  
        int nStart=0;  
        int nAll=0;  
        for(int i=0;i<length;i++){  
            nStart=max(0,A[i]+nStart);  
            nAll=max(nStart,nAll);  
        }  
        return nAll;  
    }  
    #endif 

 

 

这个问题源于《编程之美》2.14 求数组的子数组之和的最大值扩展问题2

输出子数组的最大和同时输出子数组下标,时间复杂度为O(N)
源码:

#include <iostream>
using namespace  std;
//startPos 最大和子数组的起始位置 
//endPos 最大和子数组的结束位置  
int MaxSum(int *A,int n,int &startPos,int &endPos)
{
    int tmpStart = 0;
    int nStart = A[0];
    int nAll = A[0];
    for (int i=1;i<n;i++)
     {
        if(nStart < 0)
         {
            nStart =0;
            startPos = i;
        }
        nStart += A[i];
        if (nStart>nAll)
         {
            nAll = nStart;
            endPos =i;
            tmpStart = startPos;
        }
    }
    startPos = tmpStart;
    return nAll;
}
int main()
{
    int arr[11] =  {-2,5,6,-6,4,-8,6,3,-1,2,-9};
    int startP = 0,endP = 0;
    int maxsubArrSumValue =0;
    maxsubArrSumValue = MaxSum(arr,11,startP,endP);
    cout<<maxsubArrSumValue<<endl<<startP+1<<endl<<endP+1<<endl;

    system("pause");
    return 0;
}

 

 

二维的情况

最直接的方法就是枚举

int max(int x,int y)

{

return (x>y) ? x:y;

}

 

int MaxSum(int * A,int n,int m)

{

maxinum=-INF;

for(i_min=1;i_min<=n;i_min++)

    for(i_max=i_min;i_max<=n;i_max++)

         for(j_min;j_min<=m;j_min++)

              for(j_max=j_min;j_max<=m;j_max++)

             maximum=max(maximum,Sum(A,i_min,i_max,j_min,j_max));

return maximum;

}

 二维中,假设已经确定了矩形区域的上下边界,比如知道矩形区域的上下边界分别是第a行和第c行,现在要确定左右边界。 
 
转化为一个一维问题,可以把每一列中第a行和第c行之间的元素看成一个整体。即求数组(BC[1],...BC[M])中和最大的一段,其中B[i] = B[a][i]+...B[c][i]. 

为了优化程序,我们可以先做一些预处理,并把计算结果保留下来

定义PS[i][j] 为以(1,1),(i,1),(1,j),(i,j)为边界的矩形区域元素之和

所以任意区域之和可以表示成

PS[i_max][j_max]-PS[i_min-1][j_max]-PS[i_max][j_min-1]+PS[i_min-1][j_min-1];

for(int i=0;i<=n;i++)

PS[i][0]=0;

for(int j=0;j<=M;j++)

PS[0][j]=0;

for(int i=1;i<=n;i++)

      for(int j=1;j<=M;j++)

           PS[i][j]=PS[i-1][j]+PS[i][j-1]+PS[i-1][j]-PS[i-1][j-1]+B[i][j];

 

另外的一种解法就是将二维的问题转换为一维的问题:

假设我们已经确定了矩形的上下边界分别是a,c那么现在需要确定左右边界,问题转换成求一维情况下的最大和

int MaxSum(int * A,int n,int m)

{

maximum=-INF;

for(int a=1;a<=n;a++)

     for(int c=a;c<=n;c++)

     {

         Start=BC(a,c,m);

         ALL=BC(a,c,m);

         for(int i=m-1;i>=2;i--)

             {

              if(Start<0)

              Start=0;

              Start+=BC(a,c,i);

              if(Start > ALL)

              All=Start;

             }

              if(All > maximum)

               maximum=ALL;

     }

return maximum;

}

 

推荐阅读