【UOJ671】【UNR #5】诡异操作(线段树+位运算)
- 给定一个长度为 \(n\) 的序列 \(a_{1\sim n}\)。
- 共 \(q\) 次操作,分为三种:区间除以 \(v\) 并向下取整;区间向 \(v\) 按位与;询问区间和。
- \(1\le n\le3\times10^5\),\(1\le q\le2\times10^5\),\(0\le a_i,v < 2^{128}\)
常规思路
首先容易发现两种操作都只会使 \(a_i\) 越来越小,所以对于任意一个 \(a_i\),忽略掉 \(v=1\) 的操作后,在 \(O(\log a_i)\) 次除法操作后便会变成 \(0\)。所以,对于区间除法,可以直接暴力修改。
而对于区间按位与操作,只要分别维护每一位,则 \(v\) 为 \(1\) 的位不变,\(v\) 为 \(0\) 的位清零。
这样的复杂度是 \(O((n\log V+q\log n)\log V)\),但由于 \(\log V\) 非常大,会 TLE。
奇怪的优化操作
好像之前也写过区间按位与操作的题目,然而并不知道这个优化。
常规思路是对于每一位维护数的个数,显然个数不可能超过 \(n\)。因此,把个数看作二进制数,则相当于维护了一个 \(\log V\times \log n\) 的表格。
考虑将表格翻转,变成一个 \(\log n\times\log V\) 的表格。也就是用 \(\log n\) 个变量,第 \(i\) 个变量压位维护每一位答案在二进制下第 \(i\) 位的值。
发现这样一来就有很多好处:首先,对于单独一个数需要维护的就是它自身,不需要再花 \(O(\log V)\) 的时间做二进制拆解;其次,现在只需维护 \(\log n\) 个变量,而不是 \(\log V\) 个变量,维护的时间复杂度也就变成了 \(\log n\),询问答案的时候也只要扫一遍求出 \(V_i\times2^i\) 之和即可。
所以现在的时间复杂度就被优化成了 \(O(n\log V+q\log^2n)\)。
余下的一个问题是如何上传信息,也就是实现 \(\log V\) 个压在一起的二进制数的加法。
判断进位只要判断两个相加的数以及上次进位的数是否有至少两个 \(1\)(两两按位与再按位或),而这位剩余值可以直接用不进位加法(异或)计算。
写的时候注意常数。像我一开始代码写得太丑,结果卡了一个上午才卡过去。
代码:\(O(n\log V+q\log^2n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Rg register
#define RI Rg int
#define Cn const
#define CI Cn int&
#define I inline
#define W while
#define N 300000
#define SZ 1500000
#define INT __uint128_t
using namespace std;
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
I void readINT(INT& x) {x=0;W(isspace(oc=tc()));W((x<<=4)|=isdigit(oc)?(oc&15):oc-87,!isspace(oc=tc()));}
I void writeINT(INT x) {W(OS[++OT]=(x&15)<=9?48|(x&15):87+(x&15),x>>=4);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
int n;INT a[N+5];
class SegmentTree
{
private:
#define PT CI l=1,CI r=n,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
#define PD(rt) (~F[rt]&&(T(rt<<1,F[rt]),T(rt<<1|1,F[rt]),F[rt]=-1))
int G[N<<2];INT pV[SZ],*nw,*V[N<<2],F[N<<2],E[N<<2];
I void PU(CI x)//上传信息
{
E[x]=E[x<<1]|E[x<<1|1],G[x]=max(G[x<<1],G[x<<1|1]);INT *A=V[x<<1],*B=V[x<<1|1],C=0;RI i;
for(i=0;i<=min(G[x<<1],G[x<<1|1]);++i,++A,++B) V[x][i]=*A^*B^C,C=(*A&*B)|((*A|*B)&C);//异或计算剩余值,按位与计算进位值
for(;i<=G[x<<1];++i,++A) V[x][i]=*A^C,C&=*A;for(;i<=G[x<<1|1];++i,++B) V[x][i]=*B^C,C&=*B;
C&&(V[x][++G[x]]=C);//如果最后还有进位,将位数加1
}
I void T(CI x,INT v) {if((E[x]&v)==E[x]) return;E[x]&=v,F[x]&=v;for(RI i=0;i<=G[x];++i) V[x][i]&=v;W(~G[x]&&!V[x][G[x]]) --G[x];}//将x子树内按位与v
I INT Calc(CI x) {INT t=0;for(RI i=G[x];~i;--i) (t<<=1)+=V[x][i];return t;}//计算值
public:
I SegmentTree() {nw=pV;}
I int Bd(PT)//建树,同时分配内存
{
if(F[rt]=-1,l==r) return V[rt]=nw,++nw,G[rt]=(E[rt]=V[rt][0]=a[l])?0:-1,0;
RI mid=l+r>>1,d=max(Bd(LT),Bd(RT))+1;return V[rt]=nw,nw+=d+1,PU(rt),d;
}
I void Div(CI L,CI R,INT v,PT)//区间除法
{
if(!~G[rt]) return;if(l==r) return (void)(G[rt]=(E[rt]=(V[rt][0]/=v))?0:-1);
RI mid=l+r>>1;PD(rt),L<=mid&&(Div(L,R,v,LT),0),R>mid&&(Div(L,R,v,RT),0),PU(rt);
}
I void And(CI L,CI R,INT v,PT)//区间按位与
{
if(!~G[rt]||(E[rt]&v)==E[rt]) return;if(L<=l&&r<=R) return T(rt,v);RI mid=l+r>>1;PD(rt),L<=mid&&(And(L,R,v,LT),0),R>mid&&(And(L,R,v,RT),0),PU(rt);
}
I INT Q(CI L,CI R,PT)//区间求和
{
if(!~G[rt]) return 0;if(L<=l&&r<=R) return Calc(rt);RI mid=l+r>>1;PD(rt);return (L<=mid?Q(L,R,LT):0)+(R>mid?Q(L,R,RT):0);
}
}S;
int main()
{
RI Qt,i,op,x,y;INT v;for(read(n,Qt),i=1;i<=n;++i) readINT(a[i]);S.Bd();
W(Qt--) read(op,x,y),op^3?(readINT(v),op==1?(void)(v^1&&(S.Div(x,y,v),0)):S.And(x,y,v)):writeINT(S.Q(x,y));return clear(),0;
}