问题:
现有一个含n个整数(可含负数)的序列,求x,y使a[x]+a[x+1]+……+a[y]最大.
算法:
第一级:枚举
首先想到的就是暴力枚举:从大到小枚举x,y,计算从a[x]加到a[y]的和,与当前答案比较。代码如下:
#include <cstdio>
const int maxn=1000;
int main()
{
int n,ans,a[maxn];
scanf("%d",&n);
for (int i=0;i<n;i++)
scanf("%d",&a[i]);
ans=a[0];
for (int i=0;i<n;i++)
for (int j=i;j<n;j++)
{
int sum=0;
for (int k=i;k<=j;k++) sum+=a[k];
ans=sum>ans?sum:ans;
}
printf("%d\n",ans);
return 0;
}
时间复杂度:O(n^3),对于大一点的n,n>1000时就相当吃力了。当然还要继续优化。
第二级:枚举+优化:
我们发现:当计算 [2,7] 和 [1,6] 时,我们都把[1,6] 计算了两次。实际计算真是有大量这样的浪费。
由此不难得到OI中的一个常见优化:前缀和。我们用s[i]表示a[1]+a[2]+……a[i]的和,当计算[x,y]时,我们只需计算s[y]-s[x-1]就行了,在O(1)时间内得到了元素和。
#include <cstdio>
const int maxn=1000;
int main()
{
int n,ans,a[maxn],s[maxn];
scanf("%d",&n);
s[0]=0;
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
s[i]=s[i-1]+a[i];
}
ans=a[0];
for (int i=1;i<=n;i++)
for (int j=i;j<=n;j++)
{
int sum=s[j]-s[i-1];
ans=sum>ans?sum:ans;
}
printf("%d\n",ans);
return 0;
}
时间复杂度:O(n^2),然而至此,我们还是可以再次优化。
第三级:分治算法:
首先一个序列,我们要求它的最大子序列和,只有3种情况:
(1):最大子序列全在左字串
(2):最大子序列全在右子串
(3):最大子序列头在左字串,尾在右子串
所以对于一个序列[x,y],我们先求出它左字串的最大子序列,再求出它的右子串的最大子序列,最后把结果与直接求出的(3)的子序列比较,最大值即[x,y]的最大子序列。注意:这个过程是递归的,所以求左右子串的最大子序列亦是如此,只不过当x=y时,直接返回a[x]即可(不能返回0,因为不能不选)。
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int maxn=1000;
int max_sum(int *a,int x,int y)
{
if (x==y) return a[x];
int m=x+(y-x)/2; //中间下标
int maxm=max(max_sum(a,x,m),max_sum(a,m+1,y)); //左右子串最大值
int v=0,l=a[m],r=a[m+1];
// 求出中间串最大值(贪心过程)
for (int i=m;i>=x;i--)
l=max(l,v+=a[i]);
v=0;
for (int i=m+1;i<=y;i++)
r=max(r,v+=a[i]);
return max(maxm,l+r); //比较
}
int main()
{
int n,a[maxn];
scanf("%d",&n);
for (int i=0;i<n;i++)
scanf("%d",&a[i]);
printf("%d\n",max_sum(a,0,n-1));
return 0;
}
时间复杂度?因为二分的存在,复杂度降至了O(nlogn)。
第四级:信息维护:
即便标题是分治,也有更好的算法:不难发现,最终的答案就是找到y>=x,使得s[y]-s[x]最大。
我们从前往后枚举y,远比博主机智的多的读者们已经发现了:上一次枚举的x,在这一次的枚举中不会更差。由此,我们可以维护一个”前趋最小s:last“,每次我们用当前的s[y]-last与答案比较,更大再采用,再判断当前的s[y]是否比last更小,维护即可:
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=10000;
int main()
{
int n,a[maxn],s[maxn];
scanf("%d",&n);
s[0]=0;
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
s[i]=s[i-1]+a[i];
}
int ans=a[1],min=s[1];
for (int i=2;i<=n;i++)
{
ans=max(ans,s[i]-min); //维护答案
if (s[i]<min) min=s[i]; //维护最小值
}
printf("%d\n",ans);
return 0;
}
因为x,y单调递增,所以只遍历了一遍。自此,时间复杂度降到了O(n)。
小结:
即使存在O(n)算法,这个问题最重要的实际上还是分治的算法。分治的思想可广泛运用于其他算法,像归并排序,快速排序,二分查找之类,在子串的情况分析上也闪耀着线段树的思想。分治作为OI的基本算法之一,尽管难度不高,实用性却不容小觑。
(P.S:由于博主水平问题,本节代码有可能存在Bug或是写的不够优化的地方,读者若发现,可以通过留言告知,博主定会立即修改)