最大的子序列和问题是一个很经典的问题,各种考试面试中也经常碰到。这问题的解决不难,关键是通过这个问题体会一些算法的思路,学习思考怎么解决问题。
问题是这样的:给定整数A1,A2,...,An(正负不限),Ai,...,Aj(1<=i<=j<=n)是其一个子序列,Sum为此子序列所有元素的和,对所有可能的i,j,求Sum的最大值。例如:对于输入-2,-1,11,3,5,-8,-2,9,-20,-3,5,18,-5,3,答案为23(从A11到A12)。
最直接也最容易想到的办法(当然也就是最暴力的方法),就是求出所有子序列的和,然后找出最大的。按照这个思路写出的C++代码如下:
int Method1(const vector<int> & v)
{
int max = v[0];
int size = v.size();
for(int indexSubSerBeg=0; indexSubSerBeg<size; indexSubSerBeg++)
{
for(int indexSubSerEnd=indexSubSerBeg; indexSubSerEnd<size; indexSubSerEnd++)
{
int subSerSum = v[indexSubSerBeg];
for(int i=indexSubSerBeg+1; i<indexSubSerEnd; i++)
{
subSerSum += v[i];
}
if(subSerSum > max)
{
max = subSerSum;
}
}
}
return max;
}
目测有三层循环,显然其时间复杂度是O(N^3)。数据量稍大,这速度几乎就不可容忍了。于是要想想怎么减少循环的层次。其实这就是一个思考的过程,往哪个方向去想这就是体现能力的地方。
观察一下这个序列很容易发现,-2,-1,11,3这个子序列的和就是-2,-1,11这个子序列的和加上3。这样求一个子序列的和可以利用前面计算的子序列和,能减少一些计算量。按这个思路写出的代码如下:
int Method2(const vector<int> & v)
{
int max = v[0];
int size = v.size();
for(int indexSubSerBeg=0; indexSubSerBeg<size; indexSubSerBeg++)
{
int subSerSum = 0;
for(int indexSubSerEnd=indexSubSerBeg; indexSubSerEnd<size; indexSubSerEnd++)
{
subSerSum += v[indexSubSerEnd];
if(subSerSum > max)
{
max = subSerSum;
}
}
}
return max;
}
少了一层循环,时间复杂度减少到了O(N^2)。比之前的结果要好多了。如果是要求出所有子序列的和,因为子序列有N(N+1)/2个,所以时间复杂度就只能降到这个程度了。
然而再读一下题,发现题目并没有要求我们求出所有的子序列和,只是要求求出最大的子序列和,所以应该还有优化的余地。我们再观察,看能不能再减少计算量。花点时间,应该可以发现这么两个特征:
1. 如果序列全是非正数,那么最终结果就是最大的非正数。
2. 如果序列有正数,那么最大子序列的开始位置一定是一个正数,结束位置也一定是一个正数。(这一条反证很容易)
所以我们可以找出所有从正数开始的子序列的最大和,可以尝试用一次遍历就实现。经过一番简化和验证,得到如下代码:
int Method3(const vector<int> & v)
{
int size = v.size();
assert(size>0);
int partSum = v[0];
int max = partSum;
for(int i=1;i<size;i++)
{
if(partSum<0)
partSum = 0;
partSum += v[i];
if(partSum > max)
max = partSum;
}
return max;
}
一次循环就搞定,时间复杂度为O(N)。这就是这个问题最小的时间复杂度了,因为至少要遍历子序列一遍,所以不用再想减少时间复杂度了,这算法看起来也足够简单。
但是我们还可以思考,还有没有其他的方法呢?当然是有的,不考虑效率的话,方法无数多。有一个经常提到的方法是用分治,这也是常用的思路,不过对这题来说不是那么容易想到。事实上,我就没想到。这个方法的核心思想是:把序列分成左右两部分,最大子序列和要么出现在左部分,要么出现在右部分,要么跨越两部分中间连着。于是可以递归实现。代码如下:
int Method4(const vector<int> & v, int left, int right)
{
if(left>=right)
return v[left];
int mid = (left+right)/2;
int leftSum = v[mid];
int leftMax = leftSum;
for(int i=mid-1; i>0; i--)
{
leftSum += v[i];
if(leftSum>leftMax)
leftMax = leftSum;
}
int rightSum = v[mid+1];
int rightMax = rightSum;
for(int i=mid+2; i<v.size(); i++)
{
rightSum += v[i];
if(rightSum>rightMax)
rightMax = rightSum;
}
return Max3(leftMax+rightMax, Method3(v,left,mid), Method3(v,mid+1,right));
}
int Max3(int n1, int n2, int n3)
{
int max = n1;
if(n2>max)
max = n2;
if(n3>max)
max = n3;
return max;
}
这个算法的时间复杂度是O(NlogN)。