P6578 [Ynoi2019] 魔法少女网站
题目
第十分块。
分析
操作分块+序列分块。
首先我们考虑一下不用修改的话应该怎么做。
我们可以把题目这样转化:假设 \(x\) 目前给定,设 \(b[i]=(a[i]\geq x)\) ,那么我们现在的问题就是询问区间所有为 1 的极长子区间的 \(siz*(siz+1)/2\) 之和。
那么现在 \(x\) 不给定了,相当于就是我们每次会修改一些点 \(0->1\) 或者 \(1->0\) 。
我们发现 \(1->0\) 的情况很不好维护,而 \(0->1\) 的话就相当于是合并区间或者在区间左端或者右端单点增加了(这里要分类讨论几种情况)。
那怎么让只有 \(0->1\) 的情况呢? 我们可以考虑直接把询问按照 \(x\) 升序排序,然后我们发现这个满足的数会越来越多,这样就保证了。
好了,现在我们的问题就是怎么来维护这个极长子区间呢?
考虑序列分块/链表,这里使用序列分块,同时还要记录修改,因为一会要撤回(至于为什么要撤回见后文)。
那么现在我们解决了不带修改的问题,带修改的呢?
我们不可能直接修改,因为这样依然会是我们每次会修改一些点 \(0->1\) 或者 \(1->0\) 。
这个时候的 \(1->0\) 没办法再用所谓的排序规避掉了。
那么我们可以这样来考虑:我们先不修改,把需要修改的那些点先存下来,然后把剩下的一直没有修改的点按照静态做法来做。
接下来我们对于修改了的点考虑,由于我们默认的是 0 ,于是我们现在可以对于每一次询问暴力遍历所有的修改,然后暴力判断查看这个修改会不会让我们的某一个位置 \(0->1\) ,是就改,不是就不管。
那么这样的话时间复杂度直接爆炸,是 \(O(n^2)\) 的(假设 \(n,m\) 同阶)。
那现在就需要一个新的技巧了:操作分块。
操作分块可以让我们当前的全局修改只有 \(O(B)\) 级别,而代价是增加一个每次处理当前所有块修改的复杂度,是 \(O(\frac{m}{B}\times\)重构复杂度 \()\) 。
那么现在我们对操作分块,我们的修改只有 \(O(B)\) 个了(就相当于块内修改的个数),于是我们可以使用刚刚的对于每一个询问遍历所有修改的影响的办法了。
操作分块具体实现来就是刚刚说的每个询问遍历所有修改然后处理,再撤回到这个询问遍历所有修改之前的那个状态就可以了。
取块长 \(B=\sqrt{n}\) ,这样做时间复杂度就是 \(O(n\sqrt{n})\) 的(假设 \(n,m\) 同阶),实际测试序列分块取块长 \(\sqrt{\frac{3}{4}n}\) ,操作分块取 \(\sqrt{14m}\) 要快一点。
代码
卡了一万年的常...人都傻了...
#include<bits/stdc++.h>
using namespace std;
#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
template <typename T>
inline void read(T &x){
x=0;char ch=getchar();bool f=false;
while(!isdigit(ch)){if(ch=='-'){f=true;}ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
x=f?-x:x;
return ;
}
template <typename T>
inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10^48);
return ;
}
const int N=3e5+5,M=5e3+5;
#define ll long long
ll calc[N];
int n,m,cnt1,cnt2,a[N];
struct Modify{int t,pos,val;}Q1[M];
struct Query{int t,l,r,v,id;}Q2[M];
bool b[N];
int L[M],R[M],id[N],p[N],sum[M],top;
struct Node{
int id,Ans,t1[2],t2[2];
bool type;
inline void clear(){t1[0]=t2[0]=t1[1]=t2[1]=type=0;}
}sta[M];
Node tmp;
inline void modify(const int &pos,bool type){//把pos位置修改成1,如果type为1则表示需要删除
p[pos]=pos,b[pos]=1;
tmp.id=pos;
const int las=pos-1,nex=pos+1,nowp=p[las];//las是上一个位置,nex是下一个位置,nowp是下一个位置最远的点
const bool flag1=(b[las]&&L[id[pos]]!=pos),flag2=(b[nex]&&R[id[pos]]!=pos);//分别表示这个pos和两个点都属于同一个块并且可以连接
if(!flag1&&!flag2) tmp.clear(),tmp.Ans=1;//如果都没有,那么这个点只贡献 1
else{
tmp.type=1;
if(flag1&&flag2){//如果都有,那么相当于串起来了两边
tmp.Ans=(nex-p[las])*(p[nex]-las);
tmp.t1[0]=p[las],tmp.t1[1]=p[p[las]],p[p[las]]=p[nex],
tmp.t2[0]=p[nex],tmp.t2[1]=p[p[nex]],p[p[nex]]=nowp;
}
else{
if(flag1){//只有左边
tmp.Ans=pos-p[las]+1,//这里记录的是增加量,对于原来的情况 n*(n+1)/2=(n^2+n)/2,现在就是(n+1)*(n+2)/2=(n^2+3n+2)/2,增加了 n+1
tmp.t1[0]=pos,tmp.t1[1]=p[pos],p[pos]=p[las],
tmp.t2[0]=p[las],tmp.t2[1]=p[p[las]],p[p[las]]=pos;
}
else{//只有右边
tmp.Ans=p[nex]-pos+1,//和上面同理
tmp.t1[0]=pos,tmp.t1[1]=p[pos],p[pos]=p[nex],
tmp.t2[0]=p[nex],tmp.t2[1]=p[p[nex]],p[p[nex]]=pos;
}
}
}
sum[id[pos]]+=tmp.Ans;//把变化量加上
if(type) sta[++top]=tmp;//是否需要撤回(临时修改)
return ;
}
inline void Remove(){//撤回修改
while(top){
tmp=sta[top],top--;
sum[id[tmp.id]]-=tmp.Ans,b[tmp.id]=0;
if(tmp.type==1) p[tmp.t2[0]]=tmp.t2[1],p[tmp.t1[0]]=tmp.t1[1];
}
return ;
}
inline ll query(const int &l,const int &r){
ll Ans=0;
if(id[l]==id[r]){//如果在同一块直接暴力
int cnt=0;
for(int i=l;i<=r;i++) if(b[i]) cnt++; else Ans+=calc[cnt],cnt=0;
return Ans+calc[cnt];
}
const int &BL=id[l],&BR=id[r];
int cnt1=0,cnt2=0;
for(int i=l;i<=R[BL];i++) if(b[i]) cnt1++; else Ans+=calc[cnt1],cnt1=0;//暴力两边
for(int i=r;i>=L[BR];i--) if(b[i]) cnt2++; else Ans+=calc[cnt2],cnt2=0;
int tot=cnt1;//先从第一个开始接
for(int i=BL+1;i<=BR-1;i++){
const int posl=L[i],posr=R[i];
if(p[posl]==posr) tot+=posr-posl+1;//如果一整块都是 1,直接给目前最左边区间个数加上块个数
else{
if(b[posl]) tot+=p[posl]-posl+1,Ans-=calc[p[posl]-posl+1];//如果可以继承前面的,那就继承上一个块的末尾,然后减掉这一段开头对于本身的额外贡献
Ans+=calc[tot]+sum[i],tot=0; //继承上一部分的和本身的块的贡献
if(b[posr]) tot+=posr-p[posr]+1,Ans-=calc[posr-p[posr]+1];//如果可以给后面的继承,那就存下来,把这个后继对于块的贡献减去
}
}
return Ans+calc[tot+cnt2];//不要忘记最后还有一个末尾块没加,同时还有cnt2!!
}
ll Ans[M];
int head[N],cnt;
struct Edge{int nex,to;}G[N];
inline void add(int u,int v){G[++cnt]=(Edge){head[u],v},head[u]=cnt;}
bool vis[N],use[N];
inline bool Cmp(Query x,Query y){return x.v<y.v;}
inline void Solve(){
memset(b,0,sizeof(b));
memset(p,0,sizeof(p));
memset(sum,0,sizeof(sum));//每根号n个操作都先重置一次块
for(int i=1;i<=cnt1;i++) vis[Q1[i].pos]=true;//把会修改的点标记
for(int i=1;i<=n;i++) if(!vis[i]) add(a[i],i);//如果这个点不会被修改就加边
sort(Q2+1,Q2+1+cnt2,Cmp);//把询问按照x从小到大排序
int Now=1;
for(int i=1;i<=cnt2;i++){//对于每一个询问进行处理
while(Now<=Q2[i].v){//暴力添加
for(int i=head[Now];i;i=G[i].nex) modify(G[i].to,0); //这个值对应的所有点都有,且这是永久添加,因为询问按照x升序排序
Now++;
}
for(int j=cnt1;j>=1;j--){//倒序的原因是先处理时间戳大的,因为会覆盖前面的
if(Q1[j].t<Q2[i].t&&!use[Q1[j].pos]){//如果时间戳在前面,并且这个点没有被修改
use[Q1[j].pos]=true;//这个点被修改了
if(Q1[j].val<=Q2[i].v) modify(Q1[j].pos,1);//临时修改
}
}
for(int j=1;j<=cnt1;j++){
if(!use[Q1[j].pos]){//如果这个点本身被修改,但是修改过后的 t 比当前询问大了,所以没有意义,我们把它原来的值加进去
use[Q1[j].pos]=true;
if(a[Q1[j].pos]<=Q2[i].v) modify(Q1[j].pos,1);//临时修改
}
}
Ans[Q2[i].id]=query(Q2[i].l,Q2[i].r);
Remove();//撤回临时块内修改
for(int j=1;j<=cnt1;j++) use[Q1[j].pos]=false;
}
memset(head,0,sizeof(head)),cnt=0;
for(int i=1;i<=cnt1;i++) vis[Q1[i].pos]=false;
return ;
}
int sqrtm,op,l,r,x,y;
int main(){
read(n),read(m);
sqrtm=sqrt(m*14+1);
int sqrtn=sqrt(n*(1.0*3/4)+1);
for(int i=1,c=1,j;i<=n;i=j+1,c++){
L[c]=i,R[c]=j=min(n,i+sqrtn);
for(int t=L[c];t<=R[c];t++) id[t]=c;
}
for(int i=1;i<=n;i++) calc[i]=1ll*i*(i+1)/2;
for(int i=1;i<=n;i++) read(a[i]);
for(int i=1,j;i<=m;i=j+1){
j=min(m,i+sqrtm),cnt1=cnt2=0;
for(int t=i;t<=j;t++){
read(op);
if(op==1) Q1[++cnt1].t=t,read(Q1[cnt1].pos),read(Q1[cnt1].val);
if(op==2) Q2[++cnt2].t=t,read(Q2[cnt2].l),read(Q2[cnt2].r),read(Q2[cnt2].v),Q2[cnt2].id=cnt2;
}
Solve();
for(int i=1;i<=cnt2;i++) printf("%lld\n",Ans[i]);
for(int i=1;i<=cnt1;i++) a[Q1[i].pos]=Q1[i].val;//处理块内修改
}
return 0;
}