最大子序列和问题,即给出一个序列a1,a2,a3,...,an,问该序列的所有子序列中,各项相加后和最大是多少,甚至还可能要求给出该子序列的起始位置和终止位置。
算法一:
最容易想到、直观的算法,就是枚举子序列的任何一个起点和终点位置,这样一定可以得到正确答案,并且也能求出子序列的位置,但同时也可以确定的是这样做的时间复杂度太高,达到了o(n^3),程序执行效率不高,一般都会超过时间限制。实现代码如下:
// o(n^3)算法
int MaxSubsequenceSum(int *seque, int size)
{
int maxSum = seque[0];
// i为子序列的左边界
for(int i=0; i<size; ++i)
{
// j为子序列的右边界
for(int j=i; j<size; ++j)
{
int subSum = 0;
// 迭代子序列中的每一个元素,求和
for(int k=i; k<=j; ++k) subSum += seque[k];
if(subSum > maxSum) maxSum = subSum;
}
}
return maxSum;
}
算法二:
算法一使用了三层循环,但其实最里面的那一层循环,即通过循环来计算某一个子序列的和,这里是可以避免的,可以使用累加的思想来代替循环求取子序列的和,即将复杂度降为o(n^2)。试想如果起点i确定,在枚举终点j的时候,由于j是逐渐增大的,所以当前的sum(i,j),不就是前面计算过的sum(i,j-1),然后再加上aj么。实现代码如下:
// o(n^2)算法
int MaxSubsequenceSum(int *seque, int size)
{
int maxSum = seque[0];
for(int i=0; i<size; ++i)
{
// 维护前缀和
int subSum = 0;
for(int j=i; j<size; ++j)
{
subSum += seque[j];
if(subSum > maxSum) maxSum = subSum;
}
}
return maxSum;
}
算法三:
这里介绍一种o(nlongn)的算法,其根本思想是分治。根据分治三部曲,首先将序列不断二分,最终和最大的子序列出现的位置不过以下三种可能:
1、完全在左半部
2、完全在右半部
3、跨越左右两个部分
对于1、2情况,直接递归就可以找出正确答案;对于3情况,可以以中点为中心,向左算出包含左半部分最后一个元素的最大子序列和,向右算出包含右半部份第一个元素的最大子序列和,那么答案就是它们的和。实现代码如下:
// o(nlogn)算法
int Max3(int a, int b, int c)
{
// 返回三个数中的最大值
if(a < b) a = b;
if(a > c) return a;
else return c;
}
int MaxSubsequenceSum(int *seque, int left, int right)
{
if(left == right)
{
if(seque[left] > 0) return seque[left];
// 保证最小值为0
else return 0;
}
int mid = left + (right-left)/2;
// 递归调用,求左部分的最大和
int maxLeftSum = MaxSubsequenceSum(seque, left, mid);
// 递归调用,求右部分的最大和
int maxRightSum = MaxSubsequenceSum(seque, mid+1, right);
// 定义左边界子序列的和
int leftBorderSum=0, maxLeftBorderSum=0;
for(int i=mid; i>=left; --i)
{
leftBorderSum += seque[i];
if(leftBorderSum > maxLeftBorderSum)
{
maxLeftBorderSum = leftBorderSum;
}
}
// 定义右边界子序列的和
int rightBorderSum=0, maxRightBorderSum=0;
for(int i=mid+1; i<=right; ++i)
{
rightBorderSum += seque[i];
if(rightBorderSum > maxRightBorderSum)
{
maxRightBorderSum = rightBorderSum;
}
}
// 选出这三者中的最大值并返回
return Max3(maxLeftSum, maxRightSum, maxLeftBorderSum+maxRightBorderSum);
}
算法四:
其实对于最大子序列和问题,还有时间复杂度仅为o(n)的算法,此算法基于以下分析:
如果ai是负数,那么它不可能代表最优序列的起点,因为任何包含ai的作为起点的子序列都可以通过使用a[i+1]作为起点得到改进。类似的,任何负的子序列也不可能是最优子序列的前缀(原理相同)。如果在内循环中检测到从ai到aj的子序列的和是负数,那么可以向后推进i。关键的结论是:我们不仅能够把i推进到 i+1,而且实际上我们还可以把它一直推进到j+1。该算法的一个附带优点是:它只对数据进行一次扫描,一旦ai被读入并处理,它就不再需要被记忆,并且在任意时刻,算法都能对它已经读入的数据给出最大子序列和问题的正确答案,具有这种特性的算法叫做“联机算法”,仅需要常量空间并以线性时间运行。实现代码如下:
// o(n)算法,但无法求出位置
int MaxSubsequenceSum(int *seque, int size)
{
int maxSum=seque[0], subSum=0;
for(int i=0; i<size; ++i)
{
subSum += seque[i];
if(subSum > maxSum) maxSum = subSum;
else if(subSum < 0) subSum = 0;
}
return maxSum;
}
算法五:
算法四的时间复杂度是最优的了,但是它有一个不足,就是只能求出最大子序列的和,而不能求出其位置,因为在实现上并没有记录终点。这里再讨论另外一种算法,时间复杂度同样是o(n^2),但是同时也能求出位置信息。其算法实现依赖的是动态规划的思想,设subSum[i]为以a[i]终止且包含a[i]的最大序列的和,则有:
1、如果subSum[i]<=0,那么对于subSum[i+1]来说,它将没影响,甚至减少subSum[i+1]的值,那么subSum[i+1]不应该考虑加上它,而应该直接等于a[i+1]
2、否则,如果subSum[i]>0,那么它将增加subSum[i+1]的值,所以subSum[i+1]应该等于subSum[i] + a[i+1]
所以,可以得出转移方程:subSum[1] = a[1],subSum[i+1] = subSum[i]>0 ? subSum[i]+a[i+1] : a[i+1]。实现代码如下:
// o(n)算法
int MaxSubsequenceSum(int *seque, int size, int &left, int &right)
{
int start, maxSum, subSum;
// 初始化当前子序列和最大子序列为seque[0]
start = left = right = 0;
maxSum = subSum = seque[0];
for(int i=1; i<size; ++i)
{
if(subSum > 0) subSum += seque[i];
else
{
// 抛弃当前子序列
subSum = seque[i];
// 开始新的子序列搜索
start = i;
}
// 更新最大子序列
if(maxSum < subSum)
{
maxSum = subSum;
left = start;
right = i;
}
}
return maxSum;
}
对于最大子序列和问题还有一种扩展,即将一维序列扩展为二维序列,详见下一篇文章,传送门(点击打开链接)。