最大连续和问题的提法:
给出一个长度为n的序列A1,A2,...,An,求最大的连续和(Ai+Ai+1...+Aj)。
1、首先最容易想到的思路是进行枚举,即枚举所有的i和j,累加Ai到Aj,然后更新最大值。
实现如下:(时间复杂度为O(n^3))
#include <iostream>
using namespace std;
int a[100], n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d",&a[i]); //输入数据
int max = -0x7fffffff;
int sum = 0;
for(int i = 1; i <= n; i ++)//O(n^2)
for(int j = i; j <= n; j ++){
sum = 0;
for(int k = i; k <= j; k ++) sum += a[k];
if(sum > max) max = sum;
}
cout << max <<endl;
return 0;
}
2、对于上面的直接枚举方法,还可以进行预处理,以改进时间复杂度为O(n^2)
预处理的方法是累加前i个元素的和,并存放起来S[i],而S[i]的计算是可以递推求解的。预处理时间花费为O(n)。
然后枚举的时候,A[i..j]这一子序列的和就可以在O(1)时间内求出来了。A[i..j] = S[j] - S[i-1].
实现如下:(时间复杂度为O(n^2))
#include <iostream>
using namespace std;
int a[100], s[100], n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d",&a[i]); //输入数据
//预处理,求累加和
s[0] = 0;
for(int i = 1; i <= n; i ++) s[i] = s[i-1] + a[i];
//枚举i,j
int max = -0x7fffffff;
int sum = 0;
for(int i = 1; i <= n; i ++)//O(n^2)
for(int j = i; j <= n; j ++){
sum = s[j] - s[i-1];
if(sum > max) max = sum;
}
cout << max <<endl;
return 0;
}
3、根据《算法竞赛入门经典》一书上的说法,还可以得到O(n)的解法。
即sum = s[j] - s[i-1],而i<j,故对于确定的j,要使sum最大,让s[i-1]最小即可,可以维护一个s的最小值,从而直接使用。
实现如下:(时间复杂度为O(n))
#include <iostream>
using namespace std;
int a[100], s[100], n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d",&a[i]); //输入数据
//预处理,求累加和
s[0] = 0;
for(int i = 1; i <= n; i ++) s[i] = s[i-1] + a[i];
//枚举i,j
int max = -0x7fffffff;
int min = s[0];
for(int i = 1; i <= n; i ++){
if(s[i] - min > max) max = s[i] - min;
if(s[i] < min) min = s[i];//min保存了s[1]~s[i-1]中的最小值
}
cout << max <<endl;
return 0;
}
4、分治法
首先,分治法的三个步骤:划分问题、递归求解、合并问题。
划分问题: 将序列二分
递归求解:分别求解左右两边子序列的最大连续和
合并问题:最大连续和A[i...j]可能就在左子序列(其解为LL),也可能在右子序列(其解为RR),也可能跨越了划分点(即中点,左右两边各有一部分),因此,如果是第三种情况,则以划分点为终点向左边扫描以求出最大连续和L,类似的,以划分点为起始点向右扫描,以求出最大连续和R,则问题的解为max{LL,RR,L+R}
实现如下:(时间复杂度为O(nlogn),因为合并问题花费O(n),于是:T(n) = 2T(n/2) + n,其解为O(nlogn))
#include <iostream>
using namespace std;
int a[100], s[100], n;
int max(int a, int b){return a > b ? a : b;}
int maxSubSum(int * a, int x, int y) //区间为[x,y)
{
//边界情况->只有一个元素
if(y-x == 1) return a[x];
//否则进行二分
int m = x + (y-x)/2;//学的《算法竞赛入门经典》,这样可以保证中点更靠近x
int LL = maxSubSum(a,x,m);//左子问题的解
int RR = maxSubSum(a,m,y);//右子问题的解
int sum = 0;//以m为终点向左右半序列扫描求最大连续和
int L = -0x7fffffff;
for(int i = m; i >= x; i --){
sum += a[i];
if(sum > L) L = sum;
}
int R = -0x7fffffff;
sum = 0;
for(int i = m; i < y; i ++){
sum += a[i];
if(sum > R) R = sum;
}
//合并解
int res = max(LL,RR);
res = max(res,L+R-a[m]);//左右扫描的时候a[m]各算了一次
return res;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d",&a[i]); //输入数据
cout << maxSubSum(a,1,n+1) <<endl;//注意参数的范围是开区间[1,n+1)
return 0;
}
5、总结
上面讨论了最大连续和问题的4种解法和实现的代码,另外,测试的时候用的测试用例如下:
输入:
9
1 2 -2 3 4 5 -6 7 -10
输出:
14
从可以看到,一个问题的不同解法,其时间复杂度相差甚大。从O(n^3)到O(n^2)的改进,是通过算累加和的预处理实现的。而从O(n^2)到O(n)的改进则是通过进一步的递推(维护当前所见到的最小的s[i-1])实现的。而分治法能达到O(nlogn)的时间复杂度,是因为其复杂度函数为T(n) = 2T(n/2) + n。上面的改善过程就是不断减少冗余计算的过程,应该多注意体会递推的思想、分治法的思想和步骤、预处理的方法。