整理自 http://blog.csdn.net/v_JULY_v/article/details/6444021
求子数组的最大和
题目描述:
输入一个整形数组,数组里有正数也有负数。
数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,
因此输出为该子数组的和18。
思路
一. 动态规划
设sum[i]为以第i个元素结尾且和最大的连续子数组。假设对于元素i,所有以它前面的元素结尾的子数组的长度都已经求得,那么以第i个元素结尾且和最大的连续子数组实际上,要么是以第i-1个元素结尾且和最大的连续子数组加上这个元素,要么是只包含第i个元素,即sum[i] = max(sum[i-1] + a[i], a[i])。可以通过判断sum[i-1] + a[i]是否大于a[i]来做选择,而这实际上等价于判断sum[i-1]是否大于0。由于每次运算只需要前一次的结果,因此并不需要像普通的动态规划那样保留之前所有的计算结果,只需要保留上一次的即可,因此算法的时间和空间复杂度都很小。
伪代码如下
result = a[1] sum = a[1] for i: 2 to LENGTH[a] if sum > 0 sum += a[i] else sum = a[i] if sum > result result = sum return result
二. 扫描法
(后加注:这里提到的扫描法存在一个问题就是如果最大字段和小于0则算法没法给出正确答案。其实这个问题用动态规划就好,这里的扫描法其实真的不是个好方法,只是因为很有名所以还是粘出来了)
当我们加上一个正数时,和会增加;当我们加上一个负数时,和会减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重新清零,不然的话这个负数将会减少接下来的和。实现:
//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两个大情况进行分析,于是得证。
这个算法的证明有些复杂,现在感觉应该不会错,至少思路是对的,谁帮着在表达上优化下吧。:-)
三. 分治,合并的时候穷举
(这个也是直接从原帖抄的,思路应该不难理解。鉴于复杂度高又递归,实际解决问题时不建议采用)
//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; }