题解 P4680 【[Ynoi2018]末日时在做什么?有没有空?可以来拯救吗?】
shadowice1984 · · 题解
这里是(点名被卡的)
备注:使用下面的代码的确可以AC本题,但是由于这题卡常数卡到丧心病狂,因此我并不保证这份代码可以在任意时刻AC本题(因为你谷评测姬现在不稳定性极高,人一多你的程序就变慢了……)
吐槽
我们先来看一下题目
区间加区间最大子段和
这不是b站上被暴力**穿的题吗?看我写个暴力切了它。
数据范围
笑容逐渐消失
于是你上网去查题解发现bzoj原题解居然是一个
据说跑的还没暴力快……
于是你接着去翻题解,发现了这个神奇的做法
其实这道题是道正宗计算几何题
前置芝士:凸包和凸壳
不会的话出门左转luogu模板区,包教包会
对了极角序的Graham Scan是做不了这道题的,请使用Jarvis水平步进法写这道题
前置芝士:闵科夫斯基和合并凸包
觉得太长可以先看正片,等需要学习的时候回来再看
似乎网上并没有多少讲这个东西的教程,所以我们还是详细一点讲这个东西
我们希望解决这样的问题
给定一个点集A和一个点集B,然后我们通过这样的方式生成一个点集C
点集C的生成方式为,枚举A的所有点
(一个没什么用的概念是点集C被称为点集A和点集B的闵科夫斯基和)
现在我希望你在
为了实现一个相当快速的算法我们需要充分探究点集C的性质
首先我们先证明一个结论是如果一个点要在点集C的凸包上那么这个点必须是A的凸包上的一个点和B的凸包上的一个点加出来的
那么我们发现一个性质是,如果B中只有一个点
因此点集C可以看成是点集A按照|B|种不同方向平移形成的点集的并集
当然也可以看成是点集B按照|A|种不同方向平移形成点集的并集
那么我们会发现不是凸包上的点平移之后依然不在凸包上
所以说一个点要想是凸包上的点,它必须是两个凸包上的点加出来的,否则他就是一个不是凸包上的点平移一个方向之后的得到的点,显然不会在凸包上
因此我们可以先对A和B求一个凸包,现在的问题变成了如何求A的凸包和B的凸包这两个点集的闵科夫斯基和的凸包。
那么我们依然可以将这个点集看做凸包A以各种不同的方向平移生成的点集,或者凸包B以各种不同的方向进行平移的点集
为了方便起见我们考虑合并凸壳的情况
然后我们发现两个凸壳的闵可夫斯基和大概是长这样
图中红色点是凸包上的点
然后我们发现这张图有点像一个网格图,于是我们对于图上的一个点,如果他是点i和点j加起来得到的话,我们就将它编号为(i,j),我们以这样的方式将这个点集变成一个表格
接下来我们会发现一个有趣的事实是如果点(i,j)在凸包上,且a<i,b<j则点(a,b)一定不会在凸包上,因为从图上可以很直观的看出(i,j)将(a,b)包住了
那么这其实是一个很强的剪枝条件,它告诉我们,凸包上的点在表格中必然构成一个从
证明?自行反证一下即可
所以此时我们就有了一个优秀的算法了,这个算法基于双指针
我们先求出点集A的上凸壳和点集B的上凸壳
接下来我们维护两个指针
首先先将
然后计算点
如果一侧已经无路可走就一直向上或者向右走
最后为了避免多点共线的假凸包出现就对着得到的点集接着跑一个凸包好了
这样我们就愉快的求出了点集C的凸包
ok你没看错这是这道题的前置芝士,纯粹寄蒜几盒
准备好了吗,我们进入正题
本题题解
我们先考虑一下如果只有单点修改和区间最大子段和该怎么做
动态dp,猫老师讲过的,转移写成矩阵之后线段树维护矩阵连乘积即可
然而别忘了,早在猫老师把dp的转移写成矩阵之前就有区间最大子段和单点修改这道题了……
这道题是有一个老式做法的
线段树上每个节点记录4个值
此时我们就可以合并两个区间了,转移方程可以在网上抄/看我代码/自己手推,此处不再赘述
那么我们仔细想想,
所以我们对整个序列分块
现在我们需要滋磁的操作是一个整块加上一个值之后依然可以快速得知这个块的
首先最好解决的当然是sum了,随便维护一下即可
接下来比较困难的是
所以我们以维护最大前缀和为例讲解一下,最大后缀和的方式就请诸位自己yy了
那么假设现在我们整块被打了一个+k标记的话我们会发现我们的目标其实是最大化这样一个函数的值
显然是斜率优化问题,我们将
当然是求出这个点集的凸包然后在这个凸包上二分一下直线和凸包的切点即可了,然后切点就是使得截距最大的点了,证明的话就是由于凸包中的所有点都在直线的一侧,自然是在直线上的点代进去之后截距最大。
所以后缀和也是同理的维护了
此时我们重构一个块的复杂度还是
接下来是关键部分,如何在整块加k的情况下快速的求出ans
那么我们尝试一下仿照上边的套路,将ans写成斜率优化的形式
然后我们列出来的式子是最大化
其中
但是问题来了这里我明确的告诉你ans数组只能
笑容逐渐凝固
此时如果你略微机智一点,取块长为
好了我们发现直接求ans数组显然是要T飞的,但是我们仔细想一想,真的需要ans数组吗?
可能不需要,我们需要的是
搞什么飞机不求数组就想求凸包?
抱歉求凸包的快速算法是存在的
具体来讲这样做,我们对整个序列进行分治,假设我们现在需要求出
那么其实这个凸包是
(其中x表示区间长度,y表示这个区间的和),这些点构成一个点集C,当然你的点集有
但是呢,让我们考虑一下这个点集C的生成方式
设有一个点集A
设有一个点集B
那么点集C的生成方式就是"枚举点集A中的每一个点
如果觉得不是很明了的话可以这样想,枚举A中的点相当于枚举左端点,枚举B中的点相当于枚举右端点,这样就枚举全了跨越mid的所有区间,也就生成了点集C
等等,点集C的生成方式似乎有点眼熟……?
点集A和点集B的闵可夫斯基和啊,不懂自己去看前置芝士
所以直接求出A和B的凸包大力
最后和
算法复杂度
好的于是我们通过某些奇技淫巧搞定了重构部分的复杂度,将它控制在了
于是我们接下来就可以滋磁给整个块加+k的操作了~
然后每次询问找出这些块的
于是你信心满满的写了这个算法,交上去一看全T只剩6分
此时的算法复杂度是
没关系,加上如下剪枝即可通过本题
剪枝们
优化0
开启O3,Ofast
谨慎使用其他的优化指令,极易产生负优化
IO量较小快读可能不太好使
优化1
每次修改add标记的时候不在凸包上二分,而是维护一个当前的最优决策点p,由于区间加的都是正数,所以决策点只会向右移动,暴力的向右爬一下即可
这样的话打标记复杂度降至均摊
优化2
整个块维护一个
这样的剪枝效果还是比较明显的
优化3
在分治的过程中如果当前区间长度小于
尽管简单粗暴但是卡常效果拔群
优化4
将块长设为110
其实这个优化的意思是优化3和优化4必须联合使用,也就是说,你的递归树最多递归三层,比如说110的块长会被分治成13 14 14 14 13 14 14 14这8个区间,恰好递归3层,递归4层及更多的递归树会导致低效的代码,这里特地提到块长是因为可能根据你的常数不同会出现不同的块长,但是请务必根据你的块长调整优化3中的阈值,使得恰好递归3层
优化5
询问的区间落到同一个块中的时候直接暴力求出ans,无需求l和r
看起来然并卵的剪枝,不过我的代码最慢的点跑了1488ms所以这个优化是给卡脖子的人用的
优化6
俗话说的好,最后的剪枝剪的最狠
由于每次区间加的都是正数,所以我们观察到每次区间加的时候最大子段和的长度只会单调增加而不会减少,这个性质即使是块的一部分被加了也成立
因此我们重构的时候可以记录一下重构前ans最优决策点的x,也就是之间最大子段和的长度,分治的时候如果当前区间长度小于x就直接跳出
如果rp好一点的话通常一半的递归树就被你砍掉了
但是这个剪枝也有完全失效的时候,就是这个块是一堆非常大的负值,此时你的最大子段和长度一直是0等于没剪枝
不过这也意味这你的最优决策点无法向右爬,所以这种数据本身跑的就不是特别慢
优化7
现在luogu评测姬速度忽高忽低,如果觉得自己代码海星,交之间洗洗脸,没准能过
我写了117行,相信应该不算特别长
好了上代码~
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#include<cstdio>
#include<algorithm>
using namespace std;const int N=1e5+10;const int B=110;typedef long long ll;int n;int m;const ll minf=-(1LL<<47);//点结构体,叉积计算
struct poi{ll x;ll y;friend poi operator +(poi a,poi b){return (poi){a.x+b.x,a.y+b.y};}};
inline bool cmp(const poi& a,const poi& b,const poi& c){return (a.y-c.y)*(a.x-b.x)>=(a.y-b.y)*(a.x-c.x);}
inline bool cmp(const poi& a,const poi& b,const ll& k){return k*(b.x-a.x)>=b.y-a.y;}
inline bool cmp(const poi& a,const poi& b,const poi& c,const poi& d)
{return (c.y-d.y)*(a.x-b.x)>=(a.y-b.y)*(c.x-d.x);}
inline void chull(poi* st,int& tp)//jarvis水平步进法求凸包
{int i,r;for(i=2,r=tp,tp=1;i<=r;i++){while(tp>1&&cmp(st[tp-1],st[tp],st[i]))tp--;st[++tp]=st[i];}}
struct data//最大子段和的结构体
{
ll sum;ll l;ll r;ll ans;
friend data operator +(data a,data b)
{return (data){a.sum+b.sum,max(a.sum+b.l,a.l),max(a.r+b.sum,b.r),max(a.r+b.l,max(a.ans,b.ans))};}
};poi st[B+10];ll asum[B+10];
inline void ins(const poi& a){st[a.x].y=max(st[a.x].y,a.y);}
struct block
{
data val;int siz;poi pre[B+10];poi suf[B+10];poi ans[B+10];ll a[B+10];ll add;bool FLG;
int p1;int p2;int p3;int s1;int s2;int s3;int cut;
inline int brusolve(int l,int r)//暴力代码
{
ll sum=0;for(int i=l+1;i<=r;i++)sum+=a[i],ans[i]=(poi){i-l,sum};
for(int i=l+2;i<=r;i++)
{sum=0;poi* nw=ans+l+1;for(int j=i;j<=r;j++,++nw)sum+=a[j],nw->y=max(nw->y,sum);}
s3=r-l;chull(ans+l,s3);return s3;
}
inline int solve(int l,int r)//暴力剪枝和单调性剪枝
{
if(r-l<cut)return 0;if(r-l<=16){return brusolve(l,r);}
int mid=(l+r)/2;int t1=solve(l,mid);int t2=solve(mid,r);
for(int i=1;i<=r-l;i++)st[i]=(poi){i,minf};//插入左右两边的凸包
for(int i=l+1;i<=l+t1;i++)ins(ans[i]);for(int i=mid+1;i<=mid+t2;i++)ins(ans[i]);
ll sum=0;s1=0;for(int i=mid+1;i<=r;i++)s1++,sum+=a[i],pre[s1]=(poi){s1,sum};chull(pre,s1);
sum=0;s2=0;for(int i=mid;i>l;i--)s2++,sum+=a[i],suf[s2]=(poi){s2,sum};chull(suf,s2);
int i=1;int j=1;ins(pre[i]+suf[j]);//闵可夫斯基和合并凸包
while(i!=s1&&j!=s2)
{(cmp(pre[i],pre[i+1],suf[j],suf[j+1])?j:i)++;ins(pre[i]+suf[j]);}
if(i!=s1)for(i++;i<=s1;i++)ins(pre[i]+suf[j]);
if(j!=s2)for(j++;j<=s2;j++)ins(pre[i]+suf[j]);
chull(st,s3=r-l);for(int i=1;i<=s3;i++)ans[l+i]=st[i];return s3;
}//暴力向右爬的函数
inline void aju(poi* a,int& p,const int& t){while(p!=t)if(cmp(a[p],a[p+1],-add))break;else p++;}
inline void rebuild()
{
for(int i=1;i<=siz;i++)a[i]+=add;add=0;//判一下是否全部爬到底
if(FLG==false){FLG=true;for(int i=1;i<=siz;i++)FLG=FLG&&(a[i]>=0);}
if(FLG){val.sum=0;for(int i=1;i<=siz;i++)val.sum+=a[i];val.l=val.r=val.ans=val.sum;return;}
cut=ans[p3].x;solve(0,siz);
ll sum=0;s1=0;for(int i=1;i<=siz;i++)s1++,sum+=a[i],pre[s1]=(poi){s1,sum};chull(pre,s1);
sum=0;s2=0;for(int i=siz;i>=1;i--)s2++,sum+=a[i],suf[s2]=(poi){s2,sum};chull(suf,s2);
p1=1;aju(pre,p1,s1);p2=1;aju(suf,p2,s2);p3=1;aju(ans,p3,s3);
FLG&=(p1==s1)&&(p2==s2)&&(p3==s3);
val=(data){sum,max(pre[p1].y,0LL),max(suf[p2].y,0LL),max(ans[p3].y,0LL)};
}
inline ll calc(const poi& a){return max(a.x*add+a.y,0LL);}
inline void lb_add(ll x)
{
if(FLG){add+=x;val.sum+=siz*x;val.l=val.r=val.ans=val.sum;return;}
add+=x;aju(pre,p1,s1);aju(suf,p2,s2);aju(ans,p3,s3);
FLG&=(p1==s1)&&(p2==s2)&&(p3==s3);//暴力向右爬一下
val=(data){val.sum+siz*x,calc(pre[p1]),calc(suf[p2]),calc(ans[p3])};
}
inline data calc(int l,int r)
{
data ret=(data){0,0,0,0};
int tp=0;for(int i=l;i<=r;i++)asum[++tp]=a[i]+add;
for(int i=1;i<=tp;i++)ret.sum+=asum[i],ret.l=max(ret.l,ret.sum);ret.sum=0;
for(int i=tp;i>=1;i--)ret.sum+=asum[i],ret.r=max(ret.r,ret.sum);ll mi=0;
for(int i=1;i<=tp;i++)asum[i]+=asum[i-1];
for(int i=1;i<=tp;i++)ret.ans=max(ret.ans,asum[i]-mi),mi=min(mi,asum[i]);
return ret;
}
inline ll fstcalc(int l,int r)//如果是同一个块直接暴力就行了
{
int tp=0;for(int i=l;i<=r;i++)asum[++tp]=a[i]+add;
for(int i=1;i<=tp;i++)asum[i]+=asum[i-1];ll mi=0;ll ans=0;
for(int i=1;i<=tp;i++)ans=max(ans,asum[i]-mi),mi=min(mi,asum[i]);return ans;
}
}bl[(N/B)+10];int bi[N];int bj[N];
int main()
{
scanf("%d%d",&n,&m);//分块的一般操作
for(int i=0,t=1;i<n;i+=B,t++)
{
for(int j=1;j<=B&&i+j<=n;j++)scanf("%lld",&bl[t].a[j]),bi[i+j]=t,bj[i+j]=j;
bl[t].siz=min(n-i,B);bl[t].rebuild();
}
for(int i=1;i<=m;i++)
{
int t;int l;int r;ll x;scanf("%d%d%d",&t,&l,&r);
if(t==1)
{
scanf("%lld",&x);
if(bi[l]==bi[r]){for(int i=bj[l];i<=bj[r];i++)bl[bi[l]].a[i]+=x;bl[bi[l]].rebuild();}
else
{
for(int i=bj[l];i<=B;i++)bl[bi[l]].a[i]+=x;bl[bi[l]].rebuild();
for(int i=bi[l]+1;i<bi[r];i++)bl[i].lb_add(x);
for(int i=1;i<=bj[r];i++)bl[bi[r]].a[i]+=x;bl[bi[r]].rebuild();
}
}
else
{
if(bi[l]==bi[r]){printf("%lld\n",bl[bi[l]].fstcalc(bj[l],bj[r]));}
else
{
data ret=bl[bi[l]].calc(bj[l],B);
for(int i=bi[l]+1;i<bi[r];i++)ret=ret+bl[i].val;
ret=ret+bl[bi[r]].calc(1,bj[r]);printf("%lld\n",ret.ans);
}
}
}return 0;
}