给出一个长度为n的序列A1,A2,…,An,求最大连续和。换句话说,要求找到1<=i<=j<=n,使得Ai+Ai+1+…Aj尽量大。
暴力枚举实现
#include <iostream>
using namespace std;
int A[50]={5,9,7,3,4,1,5,8,6,7,3,6,4,0,8,6,7};
int main()
{
int n=50;
int tot=0;
int best=A[0];
//初始化最大值,为了防止连续和小于0,所以必须初始化为A[0]。
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{//检查连续子序列A[i],…,A[j]
int sum=0;
for(int k=i;k<=j;k++)
{//累加元素和
sum+=A[k];
tot++;
}
if(sum>best)
{
best=sum;
}
}
}
cout<<"best="<<best<<' '<<"tot="<<tot<<endl;
return 0;
}
tot与机器的运行速度无关。
不同机器的速度不一样,运行时间也会有所差异,但tot的值一定相同。
换句话说,它去掉了机器相关的因素,只衡量算法的“工作量”大小,
具体来说,是“加法”操作的次数。
算法分析:
1.第一层循环遍历数组A的i位从0到n,是最大连续和的左起点;
2.第二层循环遍历数组A的j位从i到n,是最大连续和的右终点;
3.第三层循环遍历数组A的k位从i到j,求出当前范围内的连续和;
4.公式:设输入规模为n时的加法操作次数
T(n)= ∑i=1n∑j=inj−i+1\displaystyle\sum_{i=1}^{n} \displaystyle\sum_{j=i}^{n} j-i+1i=1∑nj=i∑nj−i+1=∑i=1n\displaystyle\sum_{i=1}^{n}i=1∑n (n−i+1)(n−i+2)2\frac{(n-i+1)(n-i+2)}{2}2(n−i+1)(n−i+2)=n(n+1)(n+2)6\frac{n(n+1)(n+2)}{6}6n(n+1)(n+2)
连续子序列之和等于两个前缀和之差实现
#include <iostream>
using namespace std;
int A[50]={5,9,7,3,4,1,5,8,6,7,3,6,4,0,8,6,7};
int S[50];
int main()
{
int n=50,best=A[0];
S[0]=0;
for(int i=0;i<n;i++)
{
S[i]=S[i-1]+A[i];
//递推前缀和S
}
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
best=max(best,S[j]-S[i-1]);
//更新最大值
}
}
cout<<"best="<<best<<endl;
return 0;
}
算法分析:
S[i]表示第1个数到第i个数的和,S[j]-S[i-1]表示第i个数到第j个数列的和。
T(n)=∑i=1nn−i+1\displaystyle\sum_{i=1}^{n} n-i+1i=1∑nn−i+1=n(n+1)2\frac {n(n+1)}{2}2n(n+1)
分治算法实现
#include <iostream>
using namespace std;
int A[50]={5,9,7,3,4,1,5,8,6,7,3,6,4,0,8,6,7};
int maxsum(int *A,int x,int y)
{//返回数组在左闭右开区间[x,y)中的最大连续和
if(y-x==1) return A[x]; //只有一个元素,直接返回。
int m=x+(y-x)/2;
//分治第一步:划分成[x,m)和[m,y)
int maxs=max(maxsum(A,x,m),maxsum(A,m,y));
//分治第二步:递归求解
int v,L,R;
v=0,L=A[m-1];
//分治第三步:合并(1)——从分界点开始往左的最大连续和L
for(int i=m-1;i>=x;i--)
{
L=max(L,v+=A[i]);
}
v=0,R=A[m];
//分支第三步:合并(2)——从分界点开始往右的最大连续和R
for(int i=m;i<y;i++)
{
R=max(R,v+=A[i]);
}
return max(maxs,L+R); //把子问题的解与L和R比较
}
int main()
{
cout<<maxsum(A,0,51)<<endl;
return 0;
}
分治算法实现:
1.划分问题:把序列分成元素个数尽量相等的两半;
2.递归求解:分别求出完全位于左半或者完全位于右半的最佳序列;
3.合并问题:求出起点位于左半,终点位于右半的最大连续和序列,并和子问题的最优解比较。
思路分析
首先,我们可以把整个数列平均分成左右两部分,答案则会在以下三种情况中:
1、所求数列完全包含在左半部分的数列中。
2、所求数列完全包含在右半部分的数列中。
3、所求数列刚好横跨分割点,即左右数列各占一部分。
前两种情况和大问题一样,只是规模小了些,如果三个子问题都能解决,那么答案就是三个结果的最大值。
计算出:以分割点为起点向左的最大连续数列和、以分割点为起点向右的最大连续数列和,这两个结果的和就是第三种情况的答案。因为已知起点,所以这两个结果都能在O(N)的时间复杂度能算出来。
算法分析:
用递归的思路进行分析,设序列长度为n时T(n)=2T(n/2)+n,T(1)=1;
其中2T(n/2)是两次长度为n/2的递归调用,而最后的n是合并的时间(整个序列恰好扫描一遍)。
注意分界点应当是x和y的平均数m=x+(y-x)/2,这是因为运算符“/”的“取整”朝零方向(towards zero)
的取整,而不是向下取整用x+(y-x)/2来确保分界点总是靠近区间起点。
动态规划实现
#include <iostream>
using namespace std;
int A[50]={0,5,9,7,3,4,1,5,8,6,7,3,6,4,0,8,6,7};
int main()
{
int ans=A[1];
for(int i=1;i<50;i++)
{
if(A[i-1]>0)A[i]+=A[i-1];
else A[i]+=0;
if(A[i]>ans) ans=A[i];
}
cout<<ans<<endl;
return 0;
}
算法分析
用dp[n]表示以第n个数结尾的最大连续子数列的和,于是存在以下递推公式:
dp[n] = max(0, dp[n-1]) + num[n]
则整个问题的答案是max(dp[m]) | m∈[1, N]。
#include <iostream>
using namespace std;
int main()
{
int N,n,s,ans,m=0;
cin>>N>>n; //读取数组长度和数列中的第一个数
ans=s=n; //把ans初始化为数列中的的第一个数
for(int i=1;i<N;i++)
{
if(s<m) m=s;
cin>>n; s+=n;
if(s-m>ans)
ans=s-m;
}
cout<<ans<<endl;
return 0;
}
已知一个sum数组,sum[i]表示第1个数到第i个数的和,于是sum[j] - sum[i-1]表示第i个数到第j个数的和。
以第n个数为结尾的最大子数列,假设这个子数列的起点是m,于是结果为sum[n] - sum[m-1]。
并且,sum[m]必然是sum[1],sum[2]…sum[n-1]中的最小值。
这样,如果在维护计算sum数组的时候,同时维护之前的最小值, 那么答案也就出来了。
在计算前缀和的过程中维护之前得到的最小值。
它的时间复杂度是O(N),空间复杂度是O(1),这达到了理论下限!