题解 P1121 【环状最大两段子段和】

Morning_Glory

2019-07-26 15:55:55

Solution

[也许更好的阅读体验1](https://blog.csdn.net/Morning_Glory_JR/article/details/97390944) [也许更好的阅读体验2](https://www.cnblogs.com/Morning-Glory/p/11250817.html) # O(n)的贪心做法 ~~(好吧这一句只是吸引你们注意的,不过确实是贪心,确实是O(n))~~ # $\mathcal{Description}$ 给出一段环状序列,即认为$a_1$和$a_n$​是相邻的,选出其中连续不重叠且非空的两段使得这两段和最大。 --- # $\mathcal{Solution}$ ## 思路 要求两个最大子段,考虑其与最大子段的关系 我们先把序列复制一遍,求出长度小于等于$n$的最大子段 把其左端点作为起点,现在得到一个长度为$n$的子段 如图,这个最大子段的和一定是正数,旁边一定是负数 ![例1](https://img-blog.csdnimg.cn/20190726151616528.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01vcm5pbmdfR2xvcnlfSlI=,size_16,color_FFFFFF,t_70) 证明,如果旁边不是负数那么其不是最大子段 那么要再弄一个子段出来,__有且仅有__ 两种方式 * 将$r+x\ $ ~ $\ i+n-1$中的最大段取出来与最大段凑成两段 * 最大段可能是有不连续的正数一起拼出来的,也就是说中间有负数 * 如图 ![例2](https://img-blog.csdnimg.cn/20190726152059597.png) * 另一种方式就是将最大子段拆成两个子段 为什么没有另一种方式将包含了最大子段中的某一段并与不包含最大子段的某段凑成两段 既然题目要子段最大,那没道理只取最大子段中的一小段 没有另外的选择方式了 ~~(选一堆负数除外)~~ ## 实现方法 --- 于是我们要解决的问题就只有两个了 * 怎么求长度小于等于$n$的最大子段 如果没有长度限制相信大家都会求 即用$sum[i]$表示前缀和,在前面找一个最小的$sum[j]$,用$sum[i]-sum[j]$就是以$i$为结尾的最大子段了 只需$O(n)$扫一遍即可 有了长度限制的最大子段 还是要再前面找一个最小的$sum[j]$,并且要有$i-j<n\ (i-j+1\leq n)$ 用单调队列维护一下就好了,即此时最小的$sum[j]$不满足长度条件,那我们就换成满足条件的$sum[j]$ * 怎么计算答案 先求一遍最大子段 第一种情况 求出其右端点$r$到$l+n-1$的最大子段 第二种情况 可认为是减去中间的一个最小子段,把所有数变成其相反数,再求一遍最大子段即可求出要减去的数的绝对值(也就是要加上的数) 另外,这仅是考虑两个最大子段都是正数的情况,若给出的答案为负数,计算结果就会为0 对这种情况特判一下就好了 还有! 若右边全是负数并且最大子段没有负数,那么我们将最大子段人为的认为是两个子段就好了 ~~说这么多其实特判的并没有什么东西~~ --- # $\mathcal{Code}$ ```cpp /******************************* Author:Morning_Glory LANG:C++ Created Time:2019年07月26日 星期五 10时32分59秒 *******************************/ #include <cstdio> #include <fstream> #include <queue> #include <algorithm> #define ll long long #define mp make_pair using namespace std; const int maxn = 400005; //{{{cin 读入优化 struct IO{ template<typename T> IO & operator>>(T&res){ res=0; bool flag=false; char ch; while((ch=getchar())>'9'||ch<'0') flag|=ch=='-'; while(ch>='0'&&ch<='9') res=(res<<1)+(res<<3)+(ch^'0'),ch=getchar(); if (flag) res=~res+1; return *this; } }cin; //}}} int n,l1,l2,r1,r2,t,tt; ll a[maxn]; ll ans,t1,t2; //{{{_find 找l~r中的最大子段 该子段左端点为lt右端点为rt ll _find (ll l,ll r,int &lt,int &rt) { deque< pair<ll,ll> > q;//单调队列记得开long long,没开WA了,考试就炸了 ll sum=0,mx=-97865432112345678,res; q.push_front(mp(l-1,0)); for (int i=l;i<=r;++i){ sum+=a[i]; while (!q.empty()&&i-q.front().first>n) q.pop_front(); if (!q.empty()){ res=sum-q.front().second; if (res>mx){ lt=q.front().first+1,rt=i,mx=res;} } while (!q.empty()&&sum<=q.back().second) q.pop_back(); q.push_back(mp(i,sum)); } return mx; } //}}} int main() { cin>>n; for (int i=1;i<=n;++i) cin>>a[i],a[i+n]=a[i]; ans=_find(1,2*n,l1,r1);//全局最大子段 if (ans<0){ sort(a+1,a+n+1); printf("%lld\n",a[n]+a[n-1]); return 0; } t1=_find(r1+1,l1+n-1,l2,r2);//右边的最大子段 for (int i=l1;i<=r1;++i) a[i]=-a[i]; t2=_find(l1,r1,t,tt);//中间的最小子段和的绝对值 ans=max(0ll,max(t1,t2))+ans;//0是对应最后那个还有 printf("%lld\n",ans); return 0; } ``` >如有哪里讲得不是很明白或是有错误,欢迎指正 如您喜欢的话不妨点个赞收藏一下吧