第一期分块杂题与根号科技

粉酷爱

本来不打算写的。

分块,作为一种神奇的暴力数据结构,常常能用来维护一些奇奇怪怪的操作。虽然一般的题,只要正解不是分块,都会选择卡它,但在考场上想不出正解时,分块仍然是一种好的选择。

这里是第二期

CF920F SUM and REPLACE

题目传送门

点击查看

可以发现,一个数替换成约数个数,它的值经过几次后变回变成 \(2\),参考 P4145 的例子,对散块和最大值大于 \(2\) 的整块暴力修改,若修改后块的最大值小于等于 \(2\),打上标记,以后不再修改。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+10;
inline int read() {
	int s=0,x=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
	return s*x;
}
int n,a[N],bel[N],len,L[N],R[N];
int pre[N*4],maxn[N],sum[N],m;
signed main() {
	n=read();m=read();len=sqrt(n);
	for(int i=1;i<=n;++i) {
		a[i]=read();
		bel[i]=(i-1)/len+1;
		if(bel[i]!=bel[i-1])L[bel[i]]=i,R[bel[i]]=i+len-1;
		maxn[bel[i]]=max(maxn[bel[i]],a[i]);
		sum[bel[i]]+=a[i];
	}
	R[bel[n]]=n;
	for(int i=1;i<=1000000;++i)
		for(int j=i;j<=1000000;j+=i)++pre[j];
	while(m--) {
		int opt=read(),l=read(),r=read();
		if(opt==1) {
			for(int i=l;i<=r&&i<=bel[l]*len;++i) {
				sum[bel[i]]-=a[i];
				a[i]=pre[a[i]];
				sum[bel[i]]+=a[i];
			}
			maxn[bel[l]]=0;
			for(int i=L[bel[l]];i<=R[bel[l]];++i)maxn[bel[l]]=max(maxn[bel[l]],a[i]);
			for(int i=bel[l]+1;i<bel[r];++i) {
				if(maxn[i]>2) {
					maxn[i]=0;
					for(int j=(i-1)*len+1;j<=i*len;++j) {
						sum[i]-=a[j];
						a[j]=pre[a[j]];
						maxn[i]=max(maxn[i],a[j]);
						sum[i]+=a[j];
					}
				}
			}
			if(bel[l]!=bel[r]) {
				for(int i=(bel[r]-1)*len+1;i<=r;++i) {
					sum[bel[i]]-=a[i];
					a[i]=pre[a[i]];
					sum[bel[i]]+=a[i];
				}
				maxn[bel[r]]=0;
				for(int i=L[bel[r]];i<=R[bel[r]];++i)maxn[bel[r]]=max(maxn[bel[r]],a[i]);
			}
		}
		else {
			int ans=0;
			for(int i=l;i<=r&&i<=bel[l]*len;++i)ans+=a[i];
			for(int i=bel[l]+1;i<bel[r];++i)ans+=sum[i];
			if(bel[l]!=bel[r])
				for(int i=(bel[r]-1)*len+1;i<=r;++i)ans+=a[i];
			printf("%lld\n",ans);
		}
	}
	return 0;
}

P4879 ycz的妹子

题目传送门

点击查看

首先,操作 \(Q\) 就是一个很简单的区间求和,我们维护一个 \(sum\) 即可。

操作 \(C\) 和操作 \(D\) 也很简单,直接暴力改。

对于操作 \(I\),我们提前对每一个有数的地方打上标记,修改时判断该点有没有数,然后直接暴力。

注意除了操作 \(I\) 外其他的修改操作都指的是第 \(x\) 个有数的位置,所以我们用一个数组 \(g\) 记录每个块有多少个数,操作时枚举每个块,找到对应位置后再操作。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M=5e5+10;
inline int read() {
	int s=0,x=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
	return s*x;
}
int len,N=M,g[M];
int n,q,a[M],block[M],sum[M];
bool vis[M];
inline void init() {
	n=read(),q=read();
	len=sqrt(N);
	for(register int i=1;i<=N;++i) {
		block[i]=(i-1)/len+1;
		if(i>n)vis[i]=1;
	}
	for(register int i=1;i<=n;++i) {
		a[i]=read();
		sum[block[i]]+=a[i];
		g[block[i]]++;
	}
}
int find(int x) {
	int num=0,pos=1;
	while(num+g[pos]<x)num+=g[pos],pos++;
	for(int i=(pos-1)*len+1;i<=min(N,pos*len);++i) {
		if(!vis[i])num++;
		if(num==x)return i;
	}
	return 0;
}
signed main()
{
	init();
	while(q--) {
		char opt;cin>>opt;
		switch(opt) {
			case 'Q':{
				int ans=0;
				for(int i=1;i<=min(n,len);++i)ans+=a[i];
				if(block[1]!=block[n])
					for(int i=(block[n]-1)*len+1;i<=n;++i)ans+=a[i];
				for(int i=block[1]+1;i<block[n];++i)ans+=sum[i];
				printf("%lld\n",ans);
				break;
			}
			case 'C':{
				int x=read(),y=read();
				if(!vis[x])a[x]-=y,sum[block[x]]-=y;
				break;
			}
			case 'I':{
				int x=read(),y=read();
				n=max(n,x);
				if(vis[x]) {
					vis[x]=0;a[x]=y;
					g[block[x]]++;
					sum[block[x]]+=y;
				}
				else {
					int q=y-a[x];
					a[x]=y;
					sum[block[x]]+=q;
				}
				break;
			}
			case 'D':{
				int x=read(),y=find(x);
				vis[y]=1;g[block[y]]--;
				sum[block[y]]-=a[y];a[y]=0;
				break;
			}
		}
	}
	return 0;
}

CF785E Anton and Permutation

题目传送门

点击查看

根据计算可得,交换两个数 \(a_l\)\(a_r\),逆序对个数会增加二倍的区间 \([l+1,r]\) 中比 \(a_r\) 小的数的个数,减少二倍的区间 \([l+1,r]\) 中比 \(a_l\) 小的数的个数。证明略。

我们用 \(vector\) 维护每个数在块中的个数有序,然后就结束了。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10,M=2e3+10;
inline int read() {
	int s=0,x=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
	return s*x;
}
int n,k,a[N],len,bel[N];
long long ans;
vector<int>v[M];
inline void update(int l,int r) {
	long long num=0;int L=l+1,R=r-1;
	if(L<=R) {
		for(int i=L;i<=bel[L]*len&&i<=R;++i)num+=(a[i]<a[r]);
		for(int i=bel[L]+1;i<bel[R];++i)num+=lower_bound(v[i].begin(),v[i].end(),a[r])-v[i].begin();
		if(bel[L]!=bel[R])
			for(int i=(bel[R]-1)*len+1;i<=R;++i)num+=(a[i]<a[r]);
		ans+=2*num;num=0;
		for(int i=L;i<=bel[L]*len&&i<=R;++i)num+=(a[i]<a[l]);
		for(int i=bel[L]+1;i<bel[R];++i)num+=lower_bound(v[i].begin(),v[i].end(),a[l])-v[i].begin();
		if(bel[L]!=bel[R])
			for(int i=(bel[R]-1)*len+1;i<=R;++i)num+=(a[i]<a[l]);
		ans-=2*num;
	}
	if(a[l]<a[r])ans++;
	else ans--;
	if(bel[l]!=bel[r]) {
		v[bel[l]].erase(lower_bound(v[bel[l]].begin(),v[bel[l]].end(),a[l]));
		v[bel[l]].insert(upper_bound(v[bel[l]].begin(),v[bel[l]].end(),a[r]),a[r]);
		v[bel[r]].erase(lower_bound(v[bel[r]].begin(),v[bel[r]].end(),a[r]));
		v[bel[r]].insert(upper_bound(v[bel[r]].begin(),v[bel[r]].end(),a[l]),a[l]);
	}
	swap(a[l],a[r]);
}
int main() {
	n=read(),k=read();len=sqrt(n);
	for(int i=1;i<=n;++i) {
		a[i]=i;
		bel[i]=(i-1)/len+1;
		v[bel[i]].push_back(a[i]);
	}
	while(k--) {
		int l=read(),r=read();
		if(l>r)swap(l,r);
		if(l==r) {
			printf("%lld\n",ans);
			continue;
		}
		update(l,r);
		printf("%lld\n",ans);
	}
	return 0;
}

P5268 一个简单的询问

题目传送门

如果改为强制在线,阁下该如何应对?

点击查看

答案显然可以拆成:

  • 右式中整块对左式的贡献。
  • 左式中整块对右式散块的贡献。
  • 左式中散块对右式散块的贡献。

我们预处理 \(sum_{i,j}\) 表示前 \(j\) 个数对第 \(i\) 个块的贡献,这样对于前两个答案就可以快速计算,对于第三个答案,直接暴力求就可以了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e4+10,M=300;
ll ans;
int sum[M][N],cnt[N],L[N],R[N],bel[N],len,n,q,a[N];
inline void init(int p) {
	for(int i=L[p];i<=R[p];++i)cnt[a[i]]++;
	for(int i=1;i<=n;++i)sum[p][i]=sum[p][i-1]+cnt[a[i]];
	for(int i=L[p];i<=R[p];++i)cnt[a[i]]--;
}
inline void query(int l1,int r1,int l2,int r2) {
	int ql=bel[l1],qr=bel[r1],pl=bel[l2],pr=bel[r2];
	for(int i=pl+1;i<pr;++i)ans+=sum[i][r1]-sum[i][l1-1];
	for(int i=ql+1;i<qr;++i) {
		if(pl==pr)ans+=sum[i][r2]-sum[i][l2-1];
		else ans+=sum[i][R[pl]]-sum[i][l2-1]+sum[i][r2]-sum[i][L[pr]-1];
	}
	for(int i=l1;i<=r1&&i<=R[ql];++i)cnt[a[i]]++;
	if(ql!=qr)
		for(int i=L[qr];i<=r1;++i)cnt[a[i]]++;
	for(int i=l2;i<=r2&&i<=R[pl];++i)ans+=cnt[a[i]];
	if(pl!=pr)
		for(int i=L[pr];i<=r2;++i)ans+=cnt[a[i]];
	for(int i=l1;i<=r1&&i<=R[ql];++i)cnt[a[i]]--;
		if(ql!=qr)
			for(int i=L[qr];i<=r1;++i)cnt[a[i]]--;
	printf("%lld\n",ans);ans=0;
}
int main() {
	scanf("%d",&n);len=sqrt(n);
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&a[i]);
		bel[i]=(i-1)/len+1;
		if(bel[i]!=bel[i-1])L[bel[i]]=i,R[bel[i]]=min(n,i+len-1);
	}
	for(int i=1;i<=bel[n];++i)init(i);
	scanf("%d",&q);
	while(q--) {
		int l1,l2,r1,r2;
		scanf("%d %d %d %d",&l1,&r1,&l2,&r2);
		query(l1,r1,l2,r2);
	}
	return 0;
}

P4168 蒲公英

题目传送门

点击查看

分块经典之区间众数。

注意到值域有些大,所以先进行离散化。预处理两个数组 \(sum_{i,j}\)\(major_{i,j}\),分别表示块 \(i\) 到块 \(j\) 最小的众数,在前 \(i\) 个块(包括 \(i\))中 \(j\) 的出现个数。

对于 \(major\),每个块扫一遍,复杂度 \(O(n \sqrt{n})\);对于 \(sum\),枚举 \(i,j\),暴力统计每个数出现次数,复杂度 \(O(n \sqrt{n})\)

考虑如何计算答案。设 \(l\) 所在块为 \(L\)\(r\) 所在块为 \(R\),分两种情况:

  • \(R - L \le 1\),直接从 \(l\) 暴力扫到 \(r\),复杂度 \(O(\sqrt{n})\)
  • \(R - L > 1\),容易发现,答案有两种出处:整块中的众数,散块中出现过的数。整块中的众数已经预处理过,对于散块中出现过的数,它的出现次数可以通过差分得到。复杂度 \(O(\sqrt{n})\)

总复杂度 \(O(n \sqrt{n})\)

#include<bits/stdc++.h>
using namespace std;
const int N=4e4+50,M=510;
int n,m,a[N],b[N],maj[M][M],col[M][N];
int len,lstans,cnt,qq,ls,rs,t[N];
inline int read() {
	int s=0,x=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
	return s*x;
}
inline void init()
{
	cnt=(n-1)/len+1;sort(b+1,b+n+1);                                   
	qq=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;++i)a[i]=lower_bound(b+1,b+qq+1,a[i])-b;
	for(int i=1;i<=cnt;++i) {
		for(int j=(i-1)*len+1;j<=min(n,i*len);++j)col[i][a[j]]++;
		for(int j=1;j<=qq;++j)col[i][j]+=col[i-1][j];
	}
	for(int i=1;i<=cnt;++i) {
		for(int j=i;j<=cnt;++j) {
			int maxn=maj[i][j-1];
			for(int k=(j-1)*len+1;k<=min(n,j*len);++k)
				if((col[j][a[k]]-col[i-1][a[k]]>col[j][maxn]-col[i-1][maxn])||(col[j][a[k]]-col[i-1][a[k]]==col[j][maxn]-col[i-1][maxn]&&a[k]<maxn))
					maxn=a[k];
			maj[i][j]=maxn;
		}
	}
}
inline void Get(int l,int r){ls=(l-1)/len+1;rs=(r-1)/len+1;}
int main() {
	n=read();m=read();len=sqrt(n);
	for(int i=1;i<=n;++i)b[i]=a[i]=read();
	init();
	while(m--) {
		int l=(read()+lstans-1)%n+1,r=(read()+lstans-1)%n+1;
		if(l>r)swap(l,r);
		Get(l,r);int ma=0;
		if(rs-ls<=1) {
			for(int i=l;i<=r;++i)t[a[i]]++;
			for(int i=l;i<=r;++i)
				if((t[a[i]]>t[ma])||(t[a[i]]==t[ma]&&a[i]<ma))ma=a[i];
			for(int i=l;i<=r;++i)t[a[i]]=0;
		}
		else {
			ma=maj[ls+1][rs-1];
			for(int i=l;i<=len*ls;++i)t[a[i]]++;
			for(int i=len*(rs-1)+1;i<=r;++i)t[a[i]]++;
			for(int i=l;i<=len*ls;++i) {
				int m1=t[a[i]]+col[rs-1][a[i]]-col[ls][a[i]];
				int m2=t[ma]+col[rs-1][ma]-col[ls][ma];
				if((m1>m2)||(m1==m2&&a[i]<ma))ma=a[i];
			}
			for(int i=len*(rs-1)+1;i<=r;++i) {
				int m1=t[a[i]]+col[rs-1][a[i]]-col[ls][a[i]];
				int m2=t[ma]+col[rs-1][ma]-col[ls][ma];
				if((m1>m2)||(m1==m2&&a[i]<ma))ma=a[i];
			}
			for(int i=l;i<=len*ls;++i)t[a[i]]=0;
			for(int i=len*(rs-1)+1;i<=r;++i)t[a[i]]=0;
		}
		printf("%d\n",lstans=b[ma]);
	}
	return 0;
}

P3203 弹飞绵羊

题目传送门

点击查看

\(LCT\) 板子题,但我们用分块。

用两个数组 \(d_i\)\(to_i\) 表示绵羊在第 \(i\) 个点需要几次会被弹出所在块,被弹出所在块后到了哪里。

容易发现,对于每次修改,只会影响到修改所在的一块,所以直接块内暴力重构即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
inline int read() {
	int s=0,x=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
	return s*x;
}
int n,m,to[N],d[N],a[N];
int len,bel[N],st[N];
int query(int x) {
	int ans=0;
	while(x<=n)ans+=d[x],x=to[x];
	return ans;
}
int main() {
	n=read();len=sqrt(n);
	for(int i=1;i<=n;++i) {
		a[i]=read();
		bel[i]=(i-1)/len+1;
		if(bel[i]!=bel[i-1])st[bel[i]]=i;
	}
	m=read();st[bel[n]+1]=n+1;
	for(int i=n;i>=1;--i) {
		if(i+a[i]>=st[bel[i]+1])d[i]=1,to[i]=i+a[i];
		else d[i]=d[i+a[i]]+1,to[i]=to[i+a[i]];
	}
	while(m--) {
		int opt=read(),x=read()+1;
		if(opt==1)printf("%d\n",query(x));
		else {
			int k=read();a[x]=k;
			for(int i=st[bel[x]+1]-1;i>=st[bel[x]];--i) {
				if(i+a[i]>=st[bel[i]+1])d[i]=1,to[i]=i+a[i];
				else d[i]=d[i+a[i]]+1,to[i]=to[i+a[i]];
			}
		}
	}
	return 0;
}

P5443 桥梁

题目传送门

点击查看

考虑对操作分块,对当前询问分块处理,并用并查集维护。将询问和重量按从大到小排序,枚举询问,把满足要求、不会被修改的边加入并查集。

考虑被修改的边产生的影响。由于被修改的边最多不会超过 \(B\) 个(\(B\) 为块长),所以可以直接暴力插入,在询问结束后,把待修改的边暴力删掉即可。理论复杂度 \(O(q \sqrt{q}logm)\)。听说快排改成归并会把老哥消去,但我不想写了,反正都能过。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,len=1600;
inline int read() {
	int s=0,x=1;char ch=getchar_unlocked();
	while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar_unlocked();}
	while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar_unlocked();
	return s*x;
}
int n,m,q,ans[N],vis[N];
int a[N],stk[N],fa[N],siz[N];
struct edge{
	int u,v,w,id;
}e[N];
struct op{
	int wei,tim,idx;
};
inline int find(int x){return x==fa[x]?x:find(fa[x]);}
inline void merge(int x,int y) {
	x=find(x),y=find(y);
	if(x==y)return ;
	if(siz[x]<siz[y])swap(x,y);
	fa[y]=x;
	siz[x]+=siz[y];
	stk[++(*stk)]=y;
}
inline void Back(int u) {
	while((*stk)>u) {
		int x=stk[(*stk)--];
		siz[fa[x]]-=siz[x];
		fa[x]=x;
	}
}
vector<op>Q1,Q2,V;
inline bool cmp1(edge x,edge y){return x.w>y.w;}
inline bool cmp2(op x,op y){return x.wei>y.wei;}
inline void rebuild() {
	if(Q1.empty()&&Q2.empty())return ;
	sort(e+1,e+m+1,cmp1);
	sort(Q2.begin(),Q2.end(),cmp2);
	for(int i=1;i<=m;++i)a[e[i].id]=i;
	for(auto v:Q1)vis[v.idx]=-1,V.push_back((op){e[a[v.idx]].w,0,v.idx});
	for(auto v:Q1)V.push_back(v);
	(*stk)=0;
	for(register int i=1;i<=n;++i)fa[i]=i,siz[i]=1;
	int p=1,lst;
	for(register int i=0;i<Q2.size();++i) {
		while(p<=m&&Q2[i].wei<=e[p].w) {
			if(!vis[e[p].id])merge(e[p].u,e[p].v);
			p++;
		}
		lst=(*stk);
		for(auto v:V)
			if(v.tim<=Q2[i].tim)vis[v.idx]=v.wei;
		for(auto v:Q1)
			if(vis[v.idx]>=Q2[i].wei)merge(e[a[v.idx]].u,e[a[v.idx]].v);
		ans[Q2[i].tim]=siz[find(Q2[i].idx)];
		Back(lst);
	}
	for(auto v:Q1) {
		vis[v.idx]=0;
		e[a[v.idx]].w=v.wei;
	}
	Q1.clear();Q2.clear();V.clear();
}
int main() {
	n=read(),m=read();
	for(register int i=1;i<=m;++i) {
		e[i].u=read();
		e[i].v=read();
		e[i].w=read();
		e[i].id=i;
	}
	q=read();
	for(register int i=1;i<=q;++i) {
		int opt=read(),l=read(),r=read();
		opt==1?Q1.push_back((op){r,i,l}):Q2.push_back((op){r,i,l});
		if(!(i%len))rebuild();
	}
	rebuild();
	for(register int i=1;i<=q;++i) {
		if(ans[i])printf("%d\n",ans[i]);
	}
	return 0;
}

温馨提示:块长不能设为 \(\sqrt{q}\),否则你就会:

捕获

经实测,块长取 \(1500\) 左右最优。


Sqrt Tree

前置芝士:分块,\(ST\) 表,位运算,线段树。

首先思考问题:

  • 在长度为 \(n\) 的序列上,\(q\) 次询问区间和(满足结合律的算术都可以),其中 \(n,q \le 10^5\)

显然,\(ST\) 表的复杂度为 \(O(nlogn)\) 预处理,\(O(1)\) 查询,而分块为 \(O(n)\) 预处理,\(O(\sqrt{n})\) 查询。

此时,我们加强一下数据,\(n,q \le 2 \times 10^7\),此时,上述两种方法显然都不行了。考虑如何优化。

我们考虑对序列分块,将序列分成 \(\sqrt{n}\) 个块,每个块大小为 \(\sqrt{n}\),对于每个块,我们维护:

  • 块内前缀和
  • 块内后缀和

再维护一个数组 \(sum_{i,j}\) 表示第 \(i\) 块到第 \(j\) 块的答案。
然后,我们将对区间 \([l,r]\) 的查询分为两种情况:

  • \(l,r\) 不在同一块内。
  • \(l,r\) 在同一块内。

此时,可以发现,对于第一种情况,我们可以做到 \(O(1)\) 查询,但如果遇到第二种情况,我们只能暴力计算,复杂度又退化到了 \(O(\sqrt{n})\),所以还需要优化。

考虑对原序列建一棵树。对于一个长度为 \(n\) 的区间,我们将其分为 \(\sqrt{n}\) 个小区间,每个区间长度为 \(\sqrt{n}\),叶子结点的长度为 \(1\)\(2\)。整棵树的高度为 \(loglogn\),建树复杂度为 \(O(nloglogn)\)

关于树的高度分析,详见 \(OI-wiki\)

树高

此时,我们将询问的复杂度降到了 \(O(loglogn)\),但这远远不够。

容易发现,询问的复杂度瓶颈在于确定树高,于是,我们可以选择二分树的高度,这样将复杂度又降到了 \(O(logloglogn)\),此时,即使是对于 \(n=2 \times 10^7\),也能轻松应对。但我们还可以优化。

我们首先将区间长度扩大为 \(2\) 的整数次幂,然后假定每层的块大小是相同的。我们将每个区间的端点写成二进制形式。
举个例子,假设有一层,第一块表示的区间为 \([0,15]\),第二块表示的区间为 \([16,31]\),我们将它们用二进制表示:
第一层为 \([000000_2,001111_2]\),第二层为 \([010000_2,011111_2]\)。经过多组测验,我们发现,每个区间中的数在二进制上只有后 \(k\) 位不同(上述例子中 \(k=4\))。所以,我们可以在预处理时求出 \(k\) 的值,就可以实现 \(O(1)\) 查询了。

具体操作:对于一个区间 \([l,r]\),判断 \(l\)\(r\) 是否处于一个块里,我们可以直接将 \(l\)\(r\) 异或起来,判断是否 \(\le 2^k -1\) 即可。
现在考虑如何找到询问所在层。我们对于区间 \([i,n]\) ,求出 \(i\) 最高位上 \(1\) 的位置,然后对于区间 \([l,r]\),我们计算 \(l \oplus r\) 最高位上 \(1\) 的位置,这样就可以快速找到询问所在层,然后就可以 \(O(1)\) 计算了。

听说它还支持修改,不过我没备。想看的话,自己上 \(OI-wiki\) 看吧。

posted @ 2025-07-10 20:39  leizepromax  阅读(85)  评论(3)    收藏  举报