<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">本文来自CSCI 3320/8325 Data Structures,对最大子序列的求和问题作了详细的探讨。</span>
1.问题的定义
对于输入的一组连续的数据序列,我们找到其中连续的子序列,且这个连续的子序列和最大。例如,若输入的数据序列为
-2 11 -4 13 -5 -2
则该问题的答案为20(11-4+13=20).
此外,若输入的数据序列所有参数均为负数,则定义最大子序列的和为0.
2.解决方法
考虑四种不同的算法来解决这个问题;
每个算法都会解释清楚,并且在C++下通过测试;
对于每个算法,会计算最坏情况下的时间复杂度,并且通过观察相对简单的算法从中得到启示,得到改进的时间复杂度的算法。
3.算法1:穷举的办法
显然,解决这个问题最明显的的一个思路是,计算出序列的所有子序列的和,然后得到其中最大的结果;
每个子序列都有个起始元素和结尾元素,为了得到所有的起始元素和结尾元素的下标,要做一个嵌套的循环;
对于每个子序列,仍然需要一个循环去计算子序列中所有元素之和;
注意到本次算法的实现中,我们并不关注找到这个子序列,只是得到了最大子序列的和而已。
C++版本的实现:
int maxSubSum1(const vector<int> &a)
{
int maxSum = 0; // 1
for (int i=0; i<a.size(); i++) // 2
for (int j=i; j<a.size(); j++) { // 3
int thisSum = 0; // 4
for (int k=i; k<=j; k++) // 5
thisSum += a[k]; // 6
if (thisSum > maxSum) // 7
maxSum = thisSum; // 8
}
return maxSum; // 9
}
算法的分析很简单,主要是以下几点:
可以看出,每次循环迭代的最大次数由序列的长度N决定,在2,3和5三个循环中,只有6这个表达式在工作,时间复杂度为常数,即O(1),因此算法1最坏的时间复杂度为O(N^3)。
算法2:在算法1的基础上减少一个循环
通过对算法1的简单分析,算法1的第5行和第6行,对于同样的i,每次计算只是比先前的计算多一个元素,因此可以消除一个循环,得到只有连个循环的版本,此时的算法时间复杂度改进为O(N^2),C++版本的实现如下:
int maxSubSum2 (const vector<int> & a)
{
int maxSum = 0;
for (int i = 0; i < a.size(); i++) {
int thisSum = 0;
for (int j = i; j < a.size(); j++) {
thisSum += a[j];
if (thisSum > maxSum)
maxSum = thisSum;
}
}
return maxSum;
}
递归的思想很简单,将问题分为大致相等的两个部分,然后各自解决这两个部分,最后合并解决。对于本问题来说,我们把输入的序列分为两段,分别计算每段的最大子序列之和,得到两者之中的最大值。不过存在一个列外,就是当最大的子序列横跨两个子序列时,得到一个最大子序列的和,比较三者的大小,就可以得到输入序列的最大子序列的和。三种情况的举例如下:
考虑到输入序列如下:
4 -3 5 -2 -1 2 6 -2
其中前四个数为左序列,后四个数为又序列,显然对于左序列而言问题答案为6(4-3+5),对于右序列而言,问题答案为8(2+6),而在左序列中包含左序列最后一个元素的问题答案为4(4-3+5-2),在右序列中包含右序列第一个元素的问题答案为7(-1+2+6),所以跨过两个子序列的最大子序列和为11(4+7),总结上述的过程,得到算法的思路如下:
该方法有一个递归函数,此函数接受左序列和右序列的边界元素(对于左序列来说是最右边的一个元素,对于右序列来说是最左边的一个元素);
首先检查最基本的情况,即仅有一个元素;
如果不是最基本的情况,递归地检查左右两个序列,得到两者的最大子序列和;
计算包含边界元素的最大子序列和;
最后,上述的三个和中最大的那个和几位输入序列的最大子序列和。
C++ 版本的代码如下:
int maxSumRec(const vector<int> &a, int l, int r)
{
if(l == r) //base case: only one element
if(a[l] > 0)
return a[l];
else
return 0;
int c = (l+r)/2; //approximate center
int maxlsum = maxSumRec(a,0,c); //solve left part
int maxrsum = maxSumRec(a,c+1,a.size()); //solve right part
int lbsum = 0, maxlbsum = 0; //left border sum
for(int i=c;i>=l;i--){
lbsum += a[i];
if(lbsum > maxlbsum)
maxlbsum = lbsum;
}
int rbsum = 0, maxrbsum = 0; //right border sum
for(int i=c+1;i<=right;i++){
rbsum += a[i];
if(rbsum > maxrbsum)
maxrbsum = rbsum;
}
return max3(maxlsum, maxrsum, maxlbsum+maxrbsum);
}
int maxSubSum3(const vetcor<int> &a)
{
return maxSumRec(a,0,a.size()-1);
}
算法时间复杂度的分析:
考虑最基础的情况(只有一个元素),显然有T(1) = O(1),若子序列中中有着不止一个元素,递归的调用函数,每次花费的时间为T(N/2),然后计算序列中的元素之和,花费的时间为O(N).,因此时间复杂度满足如下的关系式:
T(N) = 2T(N/2) + O(N),即时间复杂度为T(N) = O(NlogN)
算法4:进一步的改进,一次循环来实现
解决这个问题的最终目标不仅要是最简单的,同时也要是最有效的,即消除到算法2中的i循环:
若a[i]为负数,则任何一个以a[i]开始的序列都能够从下一个元素开始;
任何一个子序列若是以一个负的子序列开始,均可以删除那个负的子序列;
为了消除i这个循环,我们跟踪子序列的最后一个元素的下标,一旦一个子序列的和为负数,则设定和为0,从当前的子序列的中消除。
C++版本的代码如下:
int maxSubSum4(const vector<int> &a)
{
int maxSum = 0, thisSum = 0;
for(int j=0;j<a.size();j++){
thisSum += a[j];
if(thisSum > maxSum)
maxSum = thisSum;
else if(thisSum < 0) //eliminate negative prefix
thisSum = 0;
}
return maxSum;
}
显然算法4的时间复杂度为T(N) = O(N),且算法4是一种On-line算法,这意味着这个算法只需要常数的空间且能够立即得到已经处理过的数据的答案。