最大子列和问题
问题描述:
给定K个整数组成的序列{ N1, N2, ..., NK },“连续子列”被定义为{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。现要求你编写程序,计算给定整数序列的最大子列和。
输入格式:
输入第1行给出正整数 K (<= 100000);第2行给出K个整数,其间以空格分隔。
输出格式:
在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。
输入样例:
6 -2 11 -4 13 -5 -2
输出样例:
20 完整代码:(实现的为算法4,下面有详解)
#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"
#include <iostream>
//using namespace std;
int MaxSubsequenceSum(int a[],int n);
int _tmain(int argc, _TCHAR* argv[])
{
int n = 0;
int sum = 0;
scanf("%d",&n);
int *a = new int[n];
for(int i = 0;i<n;i++)
scanf("%d",&a[i]);
sum = MaxSubsequenceSum(a,n);
std::cout<<sum<<std::endl;
system("pause");
return 0;
}
int MaxSubsequenceSum(int a[],int N)
{
int Thissum,Maxsum,i;
Thissum =0;
Maxsum =0;
int l = 0;
for(i = 0;i<N;i++)
{
Thissum += a[i];
if(Thissum > Maxsum)
{
Maxsum = Thissum;
}
else if (Thissum < 0)
{
Thissum =0;
l++;
}
}
if(l == N)
{
return 0;
}
else
return Maxsum;
}
算法一:
int MaxSubsequenceSum(int a[],int N)
{
int ThisSum,MaxSum,i,j,k;
MaxSum=0;
int l =0;
for (i=0;i<10;i++)
for(j=i;j<10;j++)
{
ThisSum=0;
for (k=i;k<=j;k++)
ThisSum+=a[k];
if(ThisSum>MaxSum)
MaxSum=ThisSum;
}
if(l == N)
{
return 0;
}
else
return Maxsum;
}
我们来分析一下这个程序,首先这个程序肯定是完全正确的。运行时间为O(N³),这完全取决于第5行和第六行,第六行有一个含于三重嵌套for循环中的O(1)语句组成。第2行的循环大小为N。
第二个循环大小为N-i,他可能要小,但也可能是N。我们必须假设最坏的情况,而这可能会使得最终的界有些大。第三个循环的大小为j-i+1,我们也要假设它的大小为N。为此总数为O(1*N*N*N)=O(N³)。语句1总的开销只是O(1),而语句7和8总共开销也只不过O(N²),因为他们只是两层循环内部的简单表达式。
我们在将结合各个语句的开销大小,更精确的分析出答案是θ(N³),而上面的估计高出个因子6(不过这并不影响,因为常数不影响数量级)。下面是具体计算的过程:
精确分析由这个式子得出
最后答案是(N³+3*N²+2*N)/6
显然这个算法过于消耗时间在第5行和第6行上的计算过分的耗时了。现在有一种改进的算法2
我们可以通过撤除一个for循环来避免立方运行时间。
下面是这个算法的函数:
算法2:
int MaxSubsequenceSum(int a[],int N)
{
int ThisSum,MaxSum,i,j;
MaxSum=0;
int l = 0;
for (i=0;i<10;i++)
{
ThisSum=0;
for(j=i;j<10;j++)
{
ThisSum+=A[j];
if(ThisSum>MaxSum)
MaxSum=ThisSum;
}
}
if(l == N)
{
return 0;
}
else
return Maxsum;
}
这个算法显然是O(N²)的还有一种更简单的算法其相对时间复杂度为O(NlogN)解法,现在我们来描述它。
这个算法可以体现递归算法的优势,该方法采用一种“分治”策略。其想法是吧问题分成两个大致相等的子问题,然后递归地对它们求解,这是分部分。“治”阶段将两个子问题的解合并到一起并可能再做些少量的附加工作,最后得到整个问题的解。
在这个问题中,最大子序列和可能出现在3个地方。或者整个出现在输入数据的左半部分,或者整个出现在右半部分。或者跨越输入数据的中部从而占据左右两半部分。前两种情况可以递归求解。第三种情况的最大和可以通过求出前半部分的最大和(包含前半部分的最后一个元素)以及后半部分的最大和(包含后半部分的第一个元素)而得到。然后将这两个和加在一起。
算法3:
static int MaxSubSum(int a[],int Left,int Right)
{
int MaxLeftSum,MaxRightSum;
int MaxLeftBorderSum,MaxRightBorderSum;
int LeftBorderSum,RightBorderSum;
int Center,i;
if (Left==Right)
{
if (a[Left]>0)
return a[Left];
else
return 0;
}
Center =(Left+Right)/2;
MaxLeftSum=MaxSubSum(A,Left,Center);
MaxRightSum=MaxSubSum(A,Center+1,Right);
MaxLeftBorderSum=0;LeftBorderSum=0;
for (i=Center;i>=Left;i--)
{
LeftBorderSum+=a[i];
if (LeftBorderSum>MaxLeftBorderSum)
MaxLeftBorderSum=RightBorderSum;
}
MaxRightBorderSum=0;RightBorderSum=0;
for (i=Center+1;i<=Right;i++)
{
RightBorderSum+=a[i];
if (RightBorderSum>MaxRightBorderSum)
MaxRightBorderSum=RightBorderSum;
}
return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum);
}
int MaxSubsequenceSum(const int a[],int N)
{
return MaxSubSum(a,0,N-1);
}
递归过程调用的一般形式是传递输入的数组以及左边界和右边界,它们界定额数组要被处理的部分。单行驱动程序通过传递数组以及边界0和N-1而启动该过程。
第一行至第四行处理基准情况。如果Left==Right,那么只有一个元素,并且当该元素非负时它就是最大和子序列。Left>Right的情况是不可能出现的,除非N是负数(不过程序中的小扰动有可能致使这种混乱产生)。第六行和第七行执行的两次递归调用。我们可以看到,递归调用总是对于小于原问题的问题进行,但程序总的小扰动有可能破换这个特性。第八行至第十二行以及第十三行至第十七行计算达到中间分界处的两个最大和的和数。这两个最大和的和为扩展到左右两边的最大和。伪例程Max3返回这三个有可能的最大和中的最大者。
显然,编程时,算法3比前面两种算法需要更多的精力。然而,程序短并不意味着程序号。正如我们在前面显示算法运行时间的表中已看到,除最小输入外,该算法比前面两个算法明显要快。
最后一种算法是最为有效的算法,
算法4:
int MaxSubsequenceSum(int a[],int N)
{
int ThisSum,MaxSum,j;
int l = 0;
ThisSum=MaxSum=0;
for(j=0;j<N;j++)
{
ThisSum+=a[j];
if(ThisSum>MaxSum)
MaxSum=ThisSum;
else if(ThisSum<0)
ThisSum=0;
}
if(l == N)
{
return 0;
}
else
return Maxsum;
}
算法4的时间复杂度为O(N),这是线性的。该算法的有点是,它只对数据进行一次扫描,一旦A[i]被读入处理,它就不再需要被记忆。因此,如果数组在磁盘或者磁带上,它就可以被顺序读入,在主存中不必储存数组的任何部分。不仅如此,在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案。具有这种特性的算法叫做联机算法.仅需要常量空间并以线性时间运行的联机算法几乎是完美的算法。