这是一道经典题目了,刘汝佳在紫书上讲解了三种方法,复杂度从O(n3)->O(n2)->O(n)。
记得高一我写这道题的时候迷迷糊糊的,对于O(n)的算法并不是很理解,今天我重新写这道题并用O(nlogn)的分治方法解决,也是为写维护数列做准备。
divide and conquer 分而治之的思想可以说是OI中最为重要的思想方法之一了,往往比起复杂度更优的O(n)算法有着对问题有更强的应变能力,适用范围更加广的优势。
最直观的例子比如说求一个数列在区间[l,r]内的和,我们可以O(n)预处理出前缀和并O(1)求解,但是如果加上单点修改,那么每次O(1)修改后都需要重新O(n)求前缀和,复杂度就上去了,而对于区间修改更是仅仅修改的复杂度就达到了O(n)。这时候我们就需要线段树这种基于分治思想的数据结构来支撑了。(相比较于同样log级别的树状数组来说,线段树能解决的问题范围更加广泛,也说明了分治的重要意义)。
对于本题的分治策略,我们先考虑解对于区间[l,r]可能出现的情况。
1:最大连续和完全在左区间[l,mid]。
2:最大连续和完全在右区间[mid+1,r]。
3:最大连续和跨越左右区间,由mid向左延伸,由mid+1向右延伸。
接下来是递归终点的确定。
显然,对于单点(区间[l,r](l==r))来说,最大连续和就是这个点代表的数值本身,连续和的起终点都为l(r)。
之后我们就可以直接照着上面的策略写程序了。
对于本题需要注意一些细节问题,题目对最大连续和子序列做了清晰的定义。
1:和最大。
2:最大和相同时起点尽量靠左。
3:最大和及起点都相同时终点尽量靠左。
且题目要求输出起终点,这里我用一个结构体记录最大和,起点,终点这三个信息。
// q.c
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int M=1000000+10;
struct Data {
int x,y,z;
bool operator > (const Data &A) {
if(x!=A.x) return x>A.x;
if(y!=A.y) return y<A.y;
return z<A.z;
}
};
int n,a[M];
Data solve(int l,int r) {
if(l==r) return (Data){a[l],l,r};
int mid=(l+r)>>1,lsum=a[mid],rsum=a[mid+1];
Data lc=solve(l,mid);
Data rc=solve(mid+1,r);
Data lp={a[mid],mid,mid};
Data rp={a[mid+1],mid+1,mid+1};
for(int i=mid-1;i>=l;lsum+=a[i],i--)
if(lsum+a[i]>=lp.x) lp.x=lsum+a[i],lp.y=i;
for(int i=mid+2;i<=r;rsum+=a[i],i++)
if(rsum+a[i]>rp.x) rp.x=rsum+a[i],rp.z=i;
Data res={lp.x+rp.x,lp.y,rp.z};
if(lc>res) res=lc;
if(rc>res) res=rc;
return res;
}
int main() {
freopen("subq.in","r",stdin);
freopen("subq.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
Data ans=solve(1,n);
printf("%d\n%d\n%d\n",ans.y,ans.z,ans.x);
return 0;
}