最大子数组问题(第4章:分治策略)

来源:互联网 发布:ps4应用程序数据损坏 编辑:程序博客网 时间:2024/05/01 19:07

求子数组的最大和
题目描述:
输入一个整形数组,数组里有正数也有负数。
数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
求所有子数组的和的最大值。要求时间复杂度为O(n)。

例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,
因此输出为该子数组的和18。

 

思路

1 当我们加上一个正数时,和会增加;当我们加上一个负数时,和会减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重新清零,不然的话这个负数将会减少接下来的和。实现:

复制代码
//copyright@ July 2010/10/18  //updated,2011.05.25.  #include <iostream.h>    int maxSum(int* a, int n)  {      int sum=0;      //其实要处理全是负数的情况,很简单,如稍后下面第3点所见,直接把这句改成:"int sum=a[0]"即可      //也可以不改,当全是负数的情况,直接返回0,也不见得不行。      int b=0;            for(int i=0; i<n; i++)      {          if(b<0)           //...              b=a[i];          else              b+=a[i];          if(sum<b)              sum=b;      }      return sum;  }    int main()  {      int a[10]={1, -2, 3, 10, -4, 7, 2, -5};      //int a[]={-1,-2,-3,-4};  //测试全是负数的用例      cout<<maxSum(a,8)<<endl;      return 0;  }    /*------------------------------------- 解释下: 例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5, 那么最大的子数组为3, 10, -4, 7, 2, 因此输出为该子数组的和18。  所有的东西都在以下俩行, 即: b  :  0  1  -1  3  13   9  16  18  13   sum:  0  1   1  3  13  13  16  18  18    其实算法很简单,当前面的几个数,加起来后,b<0后, 把b重新赋值,置为下一个元素,b=a[i]。 当b>sum,则更新sum=b; 若b<sum,则sum保持原值,不更新。。July、10/31。 
复制代码

据说这道题是《编程珠机》里面的题目,叫做扫描法,速度最快,扫描一次就求出结果,复杂度是O(n)。书中说,这个算法是一个统计学家提出的。

这个算法如此精炼简单,而且复杂度只有线性。但是我想,能想出来却非常困难,而且证明也不简单。在这里,我斗胆写出自己证明的想法:

关于这道题的证明,我的思路是去证明这样的扫描法包含了所有n^2种情况,即所有未显示列出的子数组都可以在本题的扫描过程中被抛弃。

1 首先,假设算法扫描到某个地方时,始终未出现加和小于等于0的情况。

我们可以把所有子数组(实际上为当前扫描过的元素所组成的子数组)列为三种:

1.1 以开头元素为开头,结尾为任一的子数组

1.2 以结尾元素为结尾,开头为任一的子数组

1.3 开头和结尾都不等于当前开头结尾的所有子数组

1.1由于遍历过程中已经扫描,所以算法已经考虑了。1.2确实没考虑,但我们随便找到1.2中的某一个数组,可知,从开头元素到这个1.2中的数组的加和大于0(因为如果小于0就说明扫描过程中遇到小于0的情况,不包括在大前提1之内),那么这个和一定小于从开头到这个1.2数组结尾的和。故此种情况可舍弃

1.3 可以以1.2同样的方法证明,因为我们的结尾已经列举了所有的情况,那么每一种情况和1.2是相同的,故也可以舍弃。

2 如果当前加和出现小于等于0的情况,且是第一次出现,可知前面所有的情况加和都不为0

一个很直观的结论是,如果子段和小于0,我们可以抛弃,但问题是是不是他的所有以此子段结尾为结尾而开头任意的子段也需要抛弃呢?

答案是肯定的。因为以此子段开头为开头而结尾任意的子段加和都大于0(情况2的前提),所以这些子段的和是小于当前子段的,也就是小于0的,对于后面也是需要抛弃的。也就是说,所有以之前的所有元素为开头而以当前结尾之后元素为结尾的数组都可以抛弃了。

而对于后面抛弃后的数组,则可以同样递归地用1 2两个大情况进行分析,于是得证。

这个算法的证明有些复杂,现在感觉应该不会错,至少思路是对的,谁帮着在表达上优化下吧。:-)

 

2 动态规划:设sum[i] 为前i个元素中,包含第i个元素且和最大的连续子数组,result 为已找到的子数组中和最大的。对第i+1个元素有两种选择:做为新子数组的第一个元素、放入前面找到的子数组。
sum[i+1] = max(a[i+1], sum[i] + a[i+1])
result = max(result, sum[i])
(曾经在某个博客中看的有一个人给出的动态规划思路有一些错误之处,但思路还是对的,错误之处可能是疏漏吧,结果旁人就挞伐之,这种态度很不好)

 

3 分治,合并的时候穷举即可

复制代码
//Algorithm 3:时间效率为O(n*log n)  //算法3的主要思想:采用二分策略,将序列分成左右两份。  //那么最长子序列有三种可能出现的情况,即  //【1】只出现在左部分.  //【2】只出现在右部分。  //【3】出现在中间,同时涉及到左右两部分。  //分情况讨论之。  static int MaxSubSum(const int A[],int Left,int Right)  {      int MaxLeftSum,MaxRightSum;              //左、右部分最大连续子序列值。对应情况【1】、【2】      int MaxLeftBorderSum,MaxRightBorderSum;  //从中间分别到左右两侧的最大连续子序列值,对应case【3】。      int LeftBorderSum,RightBorderSum;      int Center,i;      if(Left == Right)Base Case          if(A[Left]>0)              return A[Left];          else              return 0;          Center=(Left+Right)/2;          MaxLeftSum=MaxSubSum(A,Left,Center);          MaxRightSum=MaxSubSum(A,Center+1,Right);          MaxLeftBorderSum=0;          LeftBorderSum=0;          for(i=Center;i>=Left;i--)          {              LeftBorderSum+=A[i];              if(LeftBorderSum>MaxLeftBorderSum)                  MaxLeftBorderSum=LeftBorderSum;          }          MaxRightBorderSum=0;          RightBorderSum=0;          for(i=Center+1;i<=Right;i++)          {              RightBorderSum+=A[i];              if(RightBorderSum>MaxRightBorderSum)                  MaxRightBorderSum=RightBorderSum;          }          int max1=MaxLeftSum>MaxRightSum?MaxLeftSum:MaxRightSum;          int max2=MaxLeftBorderSum+MaxRightBorderSum;          return max1>max2?max1:max2;  }  
0 0