[COGS82] 求子序列
这是一道经典题目了,刘汝佳在紫书上讲解了三种方法,复杂度从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; }