值域倍增分块&底层分块&题解:[Ynoi2007] rgxsxrs
题解
这种 \(>x\) 的 \(-x\) 的题有一种套路是值域倍增分块。
就是把值域分成 \([1,2),[2,4),[4,8),...\) 这样 \(O(\log V)\) 个块。
然后我们对每一个块都开一个线段树维护序列。
对于块 \([2^k,2^{k+1})\) 所对应的线段树,只有那些满足 \(a_i \in [2^k,2^{k+1})\) 的位置 \(i\) 对线段树有贡献。
线段树维护的是区间 \(min,max,sum\)。
那显然查询的时候对 \(O(\log V)\) 棵线段树都问一遍就可以了,查询复杂度是 \(O(m\log n \log V)\)。
看修改,对于块 \([2^k,2^{k+1})\)。
-
\(x<2^k\):此时线段树上 \([l,r]\) 中所有数都要 \(-x\)(没有贡献的位置当然不用)。
因为 \([l,r]\) 会在线段树上拆成 \(\log\) 个子区间,我们就对其中一个子区间 \([l',r']\) 考虑。
如果 \([l',r']\) 的区间最小值 \(-x\) 之后不再属于 \([2^k,2^{k+1})\),他会掉到下面的块中。
因为总共只有 \(O(\log V)\) 个块,所以一个位置只会掉 \(O(\log V)\) 次。
所以我们直接暴力 \(O(\log n)\) 把最小值从当前线段树删去(可以用线段树二分找),加入对应块的线段树。
这个操作的总复杂度是 \(O(n\log n \log V)\)。
对于剩下的那些数直接打上区间减 tag 即可,总复杂度就是普通线段树操作的复杂度 \(O(m\log n \log V)\)。
Tip: 当然不一定只有最小值要掉,次小值可能也会,次次小值可能也会,得不断删最小值直到最小值 \(-x\) 之后不会掉下去。 -
\(2^{k+1}\le x\):啥都不用做。
-
\(2^k\le x <2^{k+1}\):此时 \([l,r]\) 中一些比较大的数需要 \(-x\)。
但因为 \(x\ge 2^k\),所以减完之后至少减半,所以只会减半 \(O(\log V)\) 次,且一定会掉到下面的块,同理暴力修改的复杂度是 \(O(n\log n \log V)\)。
所以直接暴力找到所有 \(>x\) 的叶子并修改即可。
总时间复杂度是 \(O((n+m)\log n \log V)\),空间复杂度是 \(O(n\log V)\)。
如果就这样的话,这题还不是很毒瘤,也不是很难写。
但这是 ynoi,你会愉快地 MLE。
卡空间的方法是底层分块。
其实说人话就是当线段树某个节点代表的区间长度 \(\le B\) 的时候,直接暴力查询/修改。
你稍微分析一下就会发现区间操作最多只会暴力 \(O(1)\) 个块。(即那 \(\log\) 个子区间中最多只有头和尾这两个子区间的长度 \(\le B\))。
所以此时常规线段树操作的单次复杂度变成 \(O(\log n + B)\)。
而对于 \(1,3\) 情况中那些暴力找最小值/最大值并修改他们的单点操作,复杂度也是 \(O(\log n+B)\)。
可以认为是先花 \(O(\log n)\) 的时间找到他,再花 \(O(B)\) 的时间修改它。
每棵线段树的节点数就是 \(O(\frac{n}{B})\),空间复杂度就是 \(O(\frac{n \log V}{B})\)。
理论上取 \(B=\log V\) 可以使空间变成 \(O(n)\),时间会稍微大一点。
到这里两个 trick 就讲完了,只想学这两个 trick 的可以不用往下看了。
当然 ynoi 不卡常是不可能的。
主要就是改两个块长。
倍增值域分块的底数取 \(2\) 其实是基本没希望的,而且空间是贴着过去的(实测 \(59MB\))。
如果我们按照 \([1,K),[K,K^2),[K^2,K^3),...\) 这样分:
会分成 \(\log_K V\) 个块,每个数只会掉 \(\log_K V\) 次,在情况 \(3\) 中的数至多减 \(K-1\) 次就会掉到下面的块。
所以情况 \(1\) 的复杂度会变成 \(O(n \log_K V \log n)\),情况 \(3\) 的复杂度会变成 \(O(n K \log_K V \log n)\)。
对应的如果要让空间为 \(O(n)\),\(B\) 要取 \(O(\log_K V)\)。
虽然当 \(K\) 比较大的时候,空间不一定要 \(O(n)\)。
我取了 \(K=32,B=40\)。
当然,代码最终解释权归卡常所有。
code
#include<bits/stdc++.h>
#define LL long long
#define PIIII pair<pair<LL,int>,pair<int,int>>
#define PIII pair<LL,pair<int,int>>
#define fi first
#define se second
#define ls(p) t[p].ls
#define rs(p) t[p].rs
using namespace std;
const int N=5e5+5,K=32,B=40,V=1e9,inf=2*V,N2=240000,mod=(1<<20);
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int n,T,a[N],ll,rr,xx,ID[N],tmp;
int L[35],R[35],CNT,rt[35];
int tot;
int Get_id(int x){return upper_bound(L,L+CNT+1,x)-L-1;} //返回值 x 所在值域块编号
struct node{
int l,r,ls,rs,maxn,ming,cnt,add; //注意 add 是不用 LL 的,不会超过 max(a[i])
LL sum;
void tag(int d){
add+=d; sum+=1ll*cnt*d; //注意不是 (r-l+1)*d
if(cnt) ming+=d; maxn+=d;
}
}t[N2];
void Tag(int p,int id,int l,int r,int d){ //把对应块中的所有数 +d
LL sum=0;
int cnt=0,maxn=0,ming=inf;
for(int i=l;i<=r;i++){
if(ID[i]==id){
a[i]+=d;
ID[i]=Get_id(a[i]);
cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
}
}
t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}
void pushup(int p){
t[p].sum=t[ls(p)].sum+t[rs(p)].sum;
t[p].cnt=t[ls(p)].cnt+t[rs(p)].cnt;
t[p].ming=min(t[ls(p)].ming,t[rs(p)].ming);
t[p].maxn=max(t[ls(p)].maxn,t[rs(p)].maxn);
}
void pushdown(int id,int p){ //根据我们打懒标记的规则会发现,pushdown 完之后一定不会出现有元素掉到下一个块的情况
if(t[p].add){
if(t[ls(p)].r-t[ls(p)].l+1<=B) Tag(ls(p),id,t[ls(p)].l,t[ls(p)].r,t[p].add);
else t[ls(p)].tag(t[p].add);
if(t[rs(p)].r-t[rs(p)].l+1<=B) Tag(rs(p),id,t[rs(p)].l,t[rs(p)].r,t[p].add);
else t[rs(p)].tag(t[p].add);
t[p].add=0;
}
}
void query(int p,int id,int l,int r){ //暴力查询区间的sum,min,max
LL sum=0;
int cnt=0,maxn=0,ming=inf;
for(int i=l;i<=r;i++) if(ID[i]==id) cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}
void Insert(int id,int p,int x){ //将某个位置插入线段树
if(t[p].r-t[p].l+1<=B){
a[x]-=xx; //在这个地方减掉。
ID[x]=Get_id(a[x]);
query(p,id,t[p].l,t[p].r);
return;
}
pushdown(id,p);
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) Insert(id,t[p].ls,x);
else Insert(id,t[p].rs,x);
pushup(p);
}
void modify(int p,int id,int l,int r){ //暴力把区间 >x 的 -x 并求答案
LL sum=0;
int cnt=0,maxn=0,ming=inf;
for(int i=l;i<=r;i++){
if(ID[i]==id){
if(ll<=i && i<=rr && a[i]>xx) a[i]-=xx,ID[i]=Get_id(a[i]); //注意只有在操作范围内的才改
if(ID[i]!=id){ //掉到下一个块的话要先把他加回来,不然在 insert 中会再减一次
tmp=ID[i];
a[i]+=xx;
ID[i]=Get_id(a[i]);
Insert(tmp,rt[tmp],i);
}
else cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
}
}
t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}
void erase(int p,int id,int l,int r,int val){ //把区间 [l,r] 中一个值为 val 的数 -x,并从当前线段树删掉
LL sum=0;
int cnt=0,maxn=0,ming=inf;
bool flag=false; //只能删一个
for(int i=l;i<=r;i++){
if(ID[i]==id){
if(!flag && a[i]==val){ //注意不要直接在这个地方把他 -x 不然在 insert 中 pushdown 可能会让他多减一些东西
flag=true;
tmp=Get_id(a[i]-xx);
Insert(tmp,rt[tmp],i); //掉到下一个块
}
else cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
}
}
t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}
int build(int id,int l,int r){ //建树
int p=++tot;
t[p].l=l,t[p].r=r;
if(r-l+1<=B){
query(p,id,t[p].l,t[p].r);
return p;
}
int mid=(l+r)>>1;
t[p].ls=build(id,l,mid);
t[p].rs=build(id,mid+1,r);
pushup(p);
return p;
}
void find(int id,int p,int val){
if(t[p].r-t[p].l+1<=B){
erase(p,id,t[p].l,t[p].r,val); //注意这个地方只能把最小值改掉,而不能直接把所有 >x 的全 -x,不然后面打懒标记就重复减了
return;
}
pushdown(id,p);
if(t[ls(p)].ming==val) find(id,t[p].ls,val);
else find(id,t[p].rs,val);
pushup(p);
}
void change1(int id,int p){ //情况 1 的区间修改
if(t[p].cnt==0) return; //空的,没有这个块中的值
if(t[p].r-t[p].l+1<=B){ //直接暴力
modify(p,id,t[p].l,t[p].r);
return;
}
if(ll<=t[p].l&&t[p].r<=rr){
while(t[p].cnt && t[p].ming-xx<L[id]) find(id,p,t[p].ming);
t[p].tag(-xx);
return;
}
pushdown(id,p);
int mid=(t[p].l+t[p].r)>>1;
if(ll<=mid) change1(id,t[p].ls);
if(rr>mid) change1(id,t[p].rs);
pushup(p);
}
void change2(int id,int p){ //情况 3 的区间修改,这个就很暴力了,如果当前区间的最大值 >xx 就直接往下递归,也不需要拆成 log 个子区间
if(t[p].cnt==0 || t[p].maxn<=xx) return;
if(t[p].r-t[p].l+1<=B){ //直接暴力
modify(p,id,t[p].l,t[p].r);
return;
}
pushdown(id,p);
int mid=(t[p].l+t[p].r)>>1;
if(ll<=mid) change2(id,t[p].ls);
if(rr>mid) change2(id,t[p].rs);
pushup(p);
}
PIII merge(PIII x,PIII y){return {x.fi+y.fi,{max(x.se.fi,y.se.fi),min(x.se.se,y.se.se)}};}
PIII ask(int id,int p){
if(t[p].r-t[p].l+1<=B){
query(0,id,max(ll,t[p].l),min(rr,t[p].r));
return {t[0].sum,{t[0].maxn,t[0].ming}};
}
if(ll<=t[p].l&&t[p].r<=rr) return {t[p].sum,{t[p].maxn,t[p].ming}};
pushdown(id,p);
int mid=(t[p].l+t[p].r)>>1;
PIII res={0,{0,inf}};
if(ll<=mid) res=merge(res,ask(id,t[p].ls));
if(rr>mid) res=merge(res,ask(id,t[p].rs));
return res;
}
void Init(){
L[0]=1,R[0]=1;
while(true){
++CNT;
L[CNT]=R[CNT-1]+1,R[CNT]=min(1ll*V,1ll*L[CNT]*K-1ll);
if(R[CNT]==V){break;}
}
for(int i=1;i<=n;i++) ID[i]=Get_id(a[i]);
for(int i=0;i<=CNT;i++) rt[i]=build(i,1,n);
}
signed main(){
n=read(),T=read();
for(int i=1;i<=n;i++) a[i]=read();
Init();
int lstans=0;
while(T--){
int op=read();
ll=read()^lstans,rr=read()^lstans;
if(op==1){
xx=read()^lstans;
for(int i=0;i<=CNT;i++){
if(xx<L[i]) change1(i,rt[i]);
else if(xx>=L[i]&&xx<=R[i]) change2(i,rt[i]); //注意是 <= 不是 < 因为这里是闭区间
}
}
else{
PIII ans={0,{0,inf}};
for(int i=0;i<=CNT;i++){ans=merge(ans,ask(i,rt[i]));}
lstans=ans.fi%mod;
printf("%lld %d %d\n",ans.fi,ans.se.se,ans.se.fi);
}
}
return 0;
}

浙公网安备 33010602011771号