[问题描述]
给定一个数组,求其中一段连续的子数组,使其和最大。
[算法及其实现]
据说这是一个面试的经典问题,照例咱们先来分析分析。如果所有数都是非负的,当然整个数组的和就是最大和;所以当数组中存在负数时,这个问题才有意思。(本题考虑整型数,且一切运算结果不超过int的情况)
一切先从暴力开始思考。一个很朴素的思想是,枚举子数组的起始位置、终止位置,再进行加和。所以这是个O(n^3)的算法,实现如下(c++代码):
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 #define MaxN 120 5 void Read(int *a, int &n) 6 { 7 int i; 8 printf("请输入一个整数n,表示数组的元素个数。 "); 9 scanf("%d", &n); 10 printf("请输入n个整数,用空白符隔开。 "); 11 for(i = 1; i <= n; i++) 12 scanf("%d", &a[i]); 13 } 14 int Get_Max_Sum(int *a, int n) //求a[1...n]的连续子数组最大和 15 { 16 int i, j, k; 17 int Ret, Sum; 18 Ret = a[1]; 19 for(i = 1; i <= n; i++) 20 { 21 for(j = i; j <= n; j++) 22 { 23 Sum = 0; 24 for(k = i; k <= j; k++) 25 Sum +=a[k]; 26 Ret = max(Ret, Sum); 27 } 28 } 29 return Ret; 30 } 31 int main() 32 { 33 int a[MaxN], n; 34 int Ans; 35 Read(a, n); 36 Ans = Get_Max_Sum(a, n); 37 printf("连续子数组最大和 = %d ", Ans); 38 return 0; 39 }
当然我们是不满意这样的复杂度的,所以思考优化。很容易能想到,当已知起始位置和终止位置之后,在进行加和计算是愚蠢而耗时的(因为重复计算太多),能不能把这个操作优化一下呢?答案是肯定的。有一个非常常用的技术——维护前缀和。对于原数组a[]来说,我们维护一个新的数组sum[],其中sum[i]=sigma{a[k]}_{k=1...i},那么a[i]+a[i+1]+……+a[j]=sum[j]-sum[i-1],这样一个O(n)的操作就变成O(1)的了。整体的复杂度降为O(n^2)。优化后的代码如下(c++代码):
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 #define MaxN 120 5 void Read(int *a, int &n) 6 { 7 int i; 8 printf("请输入一个整数n,表示数组的元素个数。 "); 9 scanf("%d", &n); 10 printf("请输入n个整数,用空白符隔开。 "); 11 for(i = 1; i <= n; i++) 12 scanf("%d", &a[i]); 13 } 14 int Get_Max_Sum(int *a, int n) //求a[1...n]的连续子数组最大和 15 { 16 int i, j; 17 int Ret; 18 a[0] = 0; 19 for(i = 2; i <= n; i++) 20 a[i] += a[i - 1]; //这里a[]就变为了维护前缀和的sum[] 21 Ret = a[1]; 22 for(i = 1; i <= n; i++) 23 for(j = i; j <= n; j++) 24 Ret = max(Ret, a[j] - a[i - 1]); 25 return Ret; 26 } 27 int main() 28 { 29 int a[MaxN], n; 30 int Ans; 31 Read(a, n); 32 Ans = Get_Max_Sum(a, n); 33 printf("连续子数组最大和 = %d ", Ans); 34 return 0; 35 }
但从实际考虑,O(n^2)仍然是一个很高的复杂度,我们当然希望复杂度能够更低。从复杂度上来看,比O(n^2)低的一般来说有两个量级,O(n lg n)和O(n)。先思考O(n lg n)的算法,出现对数,基本上跟二分会扯上关系,那么这个问题能否用分治来实现呢?非常幸运,是可以的。
从答案出发思考,最后得到的使和最大的一段连续的子数组,会出现在什么地方呢?将原数组从中间一分为二mid=(left+right)/2,要么答案出现在左侧,要么答案出现在右侧,要么答案横跨中间位置。前两种情况将问题转化为规模更小、本质没变的子问题了,分治的雏形大致明了了;对于后一种情况,我们很容易证明,最大和=mid向左延伸的最大值+(mid+1)向右延伸的最大值,复杂度为O(n)。所以整体复杂度为O(n lg n)。实现如下(c++代码):
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 #define MaxN 120 5 void Read(int *a, int &n) 6 { 7 int i; 8 printf("请输入一个整数n,表示数组的元素个数。 "); 9 scanf("%d", &n); 10 printf("请输入n个整数,用空白符隔开。 "); 11 for(i = 1; i <= n; i++) 12 scanf("%d", &a[i]); 13 } 14 int Get_Max_Sum(int *a, int L, int R) 15 { 16 int Ret, tmp, mid = L + (R - L) / 2; 17 int Max1, Max2; 18 int i; 19 /*单个点的情况,直接返回*/ 20 if(L == R) 21 { 22 Ret = a[L]; 23 return Ret; 24 } 25 /*从左侧得到答案*/ 26 Ret = Get_Max_Sum(a, L, mid); 27 /*从右侧得到答案*/ 28 tmp = Get_Max_Sum(a, mid + 1, R); 29 Ret = max(Ret, tmp); 30 /*横跨mid的情况,实际上是指至少包括a[mid]和a[mid+1]*/ 31 //左侧 32 tmp = 0; 33 Max1 = a[mid]; 34 for(i = mid; i >= L; i--) 35 { 36 tmp += a[i]; 37 Max1 = max(Max1, tmp); 38 } 39 //右侧 40 tmp = 0; 41 Max2 = a[mid + 1]; 42 for(i = mid + 1; i <= R; i++) 43 { 44 tmp += a[i]; 45 Max2 = max(Max2, tmp); 46 } 47 tmp = Max1 + Max2; 48 Ret = max(Ret, tmp); 49 return Ret; 50 } 51 int main() 52 { 53 int a[MaxN], n; 54 int Ans; 55 Read(a, n); 56 Ans = Get_Max_Sum(a, 1, n); 57 printf("连续子数组最大和 = %d ", Ans); 58 return 0; 59 }
到现在为止,这样的复杂度已经足够让人感到满意了。(如果说递归的实现让人担心速度或者爆栈,可以尝试手写栈等,这都是实现技术上的问题,这里笔者认为没有必要纠结)
但是人都是贪心的,之前我们说了,还有一种更低的复杂度的量级O(n),如果能达到这个级别那多好啊。确实,真的有O(n)的算法。(zyy卖了好久的关子w(゚Д゚)w)
在上一种算法中,有一种情况是当前区间的答案来自于横跨中间位置mid的子数组,其答案由mid向左延伸最大值和(mid+1)向右延伸最大值组成。由此得到启发,我们可以从某一个点向左延伸,当然一定会有一个最大值(相当于固定了子数组的末端位置)。乍一看还是一个平方复杂度的算法,实则不然。考虑当前按位置i,从位置i向前延伸得到的最大和保存在sum[i]中,而对于sum[i]的取值,要么将当前元素a[i]跟之前的加在一起为sum[i]=sum[i-1]+a[i],要么从i位置重新开始一个新的子串,即sum[i]=a[i]。sum[i]取何值与sum[i-1]有关,若sum[i-1]<0,显然应该丢掉前面的东西重新开始,否则选后者。所以最终答案是max{sum[1...n]}。
具体实现时,我们发现sum[]的元素不会重复使用,故只用一个变量就可以了,另外用一个变量来记录出现过的最大值即可。实现如下(c++代码):
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 #define MaxN 120 5 void Read(int *a, int &n) 6 { 7 int i; 8 printf("请输入一个整数n,表示数组的元素个数。 "); 9 scanf("%d", &n); 10 printf("请输入n个整数,用空白符隔开。 "); 11 for(i = 1; i <= n; i++) 12 scanf("%d", &a[i]); 13 } 14 int Get_Max_Sum(int *a, int L, int R) 15 { 16 int Sum, Ret, i; 17 Ret = Sum = a[L]; 18 for(i = L + 1; i <= R; i++) 19 { 20 if(Sum > 0) 21 Sum += a[i]; 22 else 23 Sum = a[i]; 24 Ret = max(Ret, Sum); 25 } 26 return Ret; 27 } 28 int main() 29 { 30 int a[MaxN], n; 31 int Ans; 32 Read(a, n); 33 Ans = Get_Max_Sum(a, 1, n); //求数组a[1...n]的子数组最大和 34 printf("%d ", Ans); 35 return 0; 36 }
这个算法确实应该算作DP,给出状态转移方程:
sum[0] = 0
sum[i] = max{sum[i - 1], sum[i - 1] + a[i]}
Ans = max{sum[i]}_{i=1...n}