题目描述
给定长为 \(n\) 的序列, \(m\) 次操作:
1 l r x
:将 \(l\sim r\) 中的数加上 \(x\) 。2 l r x
:将 \(l\sim r\) 中的数乘上 \(x\) 。3 l r x
:将 \(l\sim r\) 中的数修改为 \(x\) 。4 l r x
:询问 \(l\sim r\) 中 \(42,424,4242,42424,424242\) 出现次数之和。
数据范围
- \(1\le n,m\le 5\cdot 10^5\) 。
- \(1\le l\le r\le n,1\le a_i,x\le 5\cdot 10^5\) ,保证任意时刻 \(1\le a_i\le 5\cdot 10^5\) 。
时间限制 \(\texttt{700ms}\) ,空间限制 \(\texttt{64MB}\) 。
分析
先考虑如果只询问 \(M\) 的出现次数怎么做。
定义势能 \(\varphi\) 为线段树所有节点对应的区间中不超过 \(M\) 的数的个数之和,初始 \(\varphi=n\log n\) 。
对于操作 \(1,2\) , \(\varphi\) 不增;对于操作 \(3\) ,将完全相同的区间视为单点, \(\varphi\) 至多增加 \(\mathcal O(\log n)\) 。
判断完全相同只需维护区间最大最小值。
对线段树每个节点维护 \(val=\max\limits_{[l,r]}[a_i\le M]a_i\) ,加乘直接对 \(val\) 打标记,如果 \(val\gt M\) 则递归下去修改。
回答询问只需顺便维护 \(val\) 的出现次数 \(cnt\) ,如果 \(val=M\) 则贡献为 \(cnt\) ,否则为零。
递归时我们花费 \(1\) 单位的代价使 \(\varphi\) 减少 \(1\) ,时间复杂度 \(\mathcal O((n+m)\log n)\) 。
同时开 \(5\) 棵线段树就做完了?抱歉,空间不够。
如果还想维护某个变量支持加乘操作,还能判断区间内是否某个位置越过了询问的数,容易发现这是做不到的。
退而求其次,注意到值域很小,所以乘法次数不会很多,直接暴力做。对于加法,记 \(nxt_i\) 为 \(i\) 到下一个(\(\ge i\))询问的数的距离,对线段树每个节点维护 \(val=\min nxt_{a_i}\) ,加法和覆盖操作可以套用上面的做法。
于是我们需要重新定义势能:
- \(\varphi_1\) 表示线段树所有区间中大于 \(a_i\) 的询问的数个数之和。
- \(\varphi_2\) 表示线段树所有区间 \(\sum\lfloor\log a_i\rfloor\) 之和。
初始 \(\varphi_1=5n\log n,\varphi_2=n\log n\log V\) 。
对于加法操作,花费 \(1\) 的代价使 \(\varphi_1\) 减小 \(1\) ,同时 \(\varphi_2\) 不增。
对于乘法操作,花费 \(1\) 的代价使 \(\varphi_2\) 减小 \(1\) ,同时 \(\varphi_1\) 不增。
对于覆盖操作, \(\varphi_1\) 增加 \(5\) , \(\varphi_2\) 增加 \(\log V\) 。
时间复杂度 \(\mathcal O((n\log n+m)(5+\log V))\) 。
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=5e5+5;
int m,n;
int key[6]={42,424,4242,42424,424242,500001};
int a[maxn],nxt[maxn];
struct node
{int l,r;int mn,mx,cnt,val;///val=min nxtint add,cov;
}f[maxn<<2];
int read()
{int q=0;char ch=getchar();while(!isdigit(ch)) ch=getchar();while(isdigit(ch)) q=10*q+ch-'0',ch=getchar();return q;
}
void pushup(int p)
{f[p].mn=min(f[ls].mn,f[rs].mn);f[p].mx=max(f[ls].mx,f[rs].mx);f[p].val=min(f[ls].val,f[rs].val);f[p].cnt=(f[p].val==f[ls].val?f[ls].cnt:0)+(f[p].val==f[rs].val?f[rs].cnt:0);
}
void pushcov(int p,int x)
{f[p].mn=f[p].mx=x,f[p].val=nxt[x],f[p].cnt=f[p].r-f[p].l+1,f[p].cov=x,f[p].add=0;
}
void pushadd(int p,int x)
{if(f[p].cov) return pushcov(p,f[p].cov+x);assert(x<=f[p].val);f[p].mn+=x,f[p].mx+=x,f[p].val-=x,f[p].add+=x;
}
void pushdown(int p)
{assert(!f[p].cov||!f[p].add);if(f[p].cov) pushcov(ls,f[p].cov),pushcov(rs,f[p].cov),f[p].cov=0;if(f[p].add) pushadd(ls,f[p].add),pushadd(rs,f[p].add),f[p].add=0;
}
void build(int p,int l,int r)
{f[p].l=l,f[p].r=r;if(l==r) return f[p].mn=f[p].mx=a[l],f[p].val=nxt[a[l]],f[p].cnt=1,void();int mid=(l+r)>>1;build(ls,l,mid),build(rs,mid+1,r);pushup(p);
}
void modify_a(int p,int l,int r,int x)
{if(l>f[p].r||r<f[p].l) return ;if(l<=f[p].l&&f[p].r<=r){if(f[p].mn==f[p].mx) return pushcov(p,f[p].mn+x);if(x<=f[p].val) return pushadd(p,x);}pushdown(p);modify_a(ls,l,r,x),modify_a(rs,l,r,x);pushup(p);
}
void modify_m(int p,int l,int r,int x)
{if(x==1||l>f[p].r||r<f[p].l) return ;if(l<=f[p].l&&f[p].r<=r&&f[p].mn==f[p].mx) return pushcov(p,f[p].mn*x);pushdown(p);modify_m(ls,l,r,x),modify_m(rs,l,r,x);pushup(p);
}
void modify_c(int p,int l,int r,int x)
{if(l>f[p].r||r<f[p].l) return ;if(l<=f[p].l&&f[p].r<=r) return pushcov(p,x);pushdown(p);modify_c(ls,l,r,x),modify_c(rs,l,r,x);pushup(p);
}
int query(int p,int l,int r)
{if(l>f[p].r||r<f[p].l) return 0;if(l<=f[p].l&&f[p].r<=r) return !f[p].val?f[p].cnt:0;pushdown(p);return query(ls,l,r)+query(rs,l,r);
}
int main()
{n=read(),m=read();for(int i=1;i<=n;i++) a[i]=read();for(int i=1;i<=5e5;i++)for(int j=5;j>=0;j--)if(i<=key[j]) nxt[i]=key[j]-i;build(1,1,n);while(m--){int op=read(),l=read(),r=read();if(op==1) modify_a(1,l,r,read());if(op==2) modify_m(1,l,r,read());if(op==3) modify_c(1,l,r,read());if(op==4) printf("%d\n",query(1,l,r));}return 0;
}
总结
-
本题卡
scanf
,必须快读。 -
标记合并是永远的难点。由于
cov
优先级比add
高,所以如果cov
标记存在,区间加要转化为区间覆盖操作。 -
惨痛的
debug
经历:博主在第 \(55\) 行后加上了这样一句话
if(f[p].l==f[p].r) return f[p].mn+=x,f[p].mx+=x,f[p].val=nxt[f[p].mn],void();
,然后报错信息都出在几千行的位置。再加上本题数据超级难造(纯随机小数据输出全是零,大数据很难保证值域范围),调了一个多小时。后来发现
cov
标记也要更新,因为头顶的add
标记推到这里会转化为区间覆盖,需要用到cov
标记的值。一般情况下标记都是打给下一层的,但是本题我们需要用
cov
标记反推这一层的信息,总之非常魔幻就对了。