分块专题整理(分块+莫队)

1.什么是分块

说白了,分块就是优化的暴力。

例如,进行区间加法操作时,我们可以把序列分为若干个块,被全覆盖的块只需进行标记,再把没被全覆盖的进行暴力处理,进行大量大范围操作时区块相对暴力的优越性是显然的。

通常取每个块的大小为 N \sqrt N N ,这样可以达到时空平衡,时间复杂度为 O ( ( N + Q ) ∗ log ⁡ N ) O((N+Q) * \log N) O((N+Q)logN)

2.分块的作用与优缺点

与树状数组和线段树等数据结构的作用一样,分块是用来维护一个大区间的各种操作和查询。

与前两种做法相比,分块的优点就是直观,简单易懂,而且可拓展性强,可以维护很多类型的操作。

但是分块的缺点也很明显,就是效率较低,相比线段树要慢很多,而且复杂度具有随数据情况而变的不确定性,所以能使用其他优秀的做法尽量还是不要用分块,当然前提是保证正确。

3.分块的一般步骤

分块入门1为例

#include<bits/stdc++.h>
using namespace std;
int n,m,len;
int pos[101000];
//个人习惯,可以用函数代替pos数组
int a[101000];
int add[400];
//加法标记数组,一个块只需要一个
void ADD(int l,int r,int val){
	int p=pos[l],q=pos[r];//访问两端点所在块的编号
	if(p==q){
		//在同一块内暴力更新就好
		for(int i=l;i<=r;i++) a[i]+=val;
	}
	else{
		//先把p~q中被完全覆盖的块打上加法标记
		for(int i=p+1;i<q;i++) add[i]+=val;
		//对于左右两端没被完全覆盖的地方暴力更新
		for(int i=l;i<=p*len;i++) a[i]+=val;
		for(int i=(q-1)*len+1;i<=r;i++) a[i]+=val;
		//个人习惯计算每一块的左右端点,可以提前预处理每一块的左右端点,存在数组中访问
	}
}
int get(int id){
	return a[id]+add[pos[id]];
}
int main(){
	scanf("%d",&n);m=n;len=sqrt(n);//默认块长为sqrt(n)
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		pos[i]=(i-1)/len+1;//预处理每个位置所在块的编号
	}
	while(m--){
		int op,l,r,c;
		scanf("%d%d%d%d",&op,&l,&r,&c);
		if(op==0) ADD(l,r,c);
		else printf("%d\n",get(r));
	}
	return 0;
} 

对于一些特殊的题目,进行的操作不能简单的处理。

这里以分块入门8为例

这道题的操作时把整个区间变为某个数字,显然想上一题那样累加标记是无法完成的,需要通过其他方法维护标记数组。

#include<bits/stdc++.h>
using namespace std;
//
int n,m,len;
int pos[101000];
int a[101000];
int now[400];
//表示现在这一块整体是哪个数字,如果块内数字不同,则该块值为-1。
int get(int l,int r,int c){
	int p=pos[l],q=pos[r];
	if(p==q){
		//在同一块内,暴力查询。
		int cnt=0;
		//该块的标记就是目标值,直接统计
		if(now[p]==c) return r-l+1;
		//否则就暴力统计有多少个目标值
		else if(now[p]==-1) for(int i=l;i<=r;i++) cnt+=a[i]==c?1:0;
		return cnt;
	}
	else {
		int cnt=0;
		for(int i=p+1;i<=q-1;i++) {
			if(now[i]==c) cnt+=len;
			//标记值为目标值,统计上该块内的所有元素
			else if(now[i]==-1) {
				for(int k=(i-1)*len+1;k<=i*len;k++){
					cnt+=a[k]==c?1:0;
				}
				//如果该块内不是统一的,暴力查找。
			}
			//如果块内统一且标记不是目标值,直接忽略。
		}
		//与上一步类似的未完全覆盖块的统计
		if(now[p]==c) cnt+=p*len-l+1;
		else if(now[p]==-1) for(int i=l;i<=p*len;i++) cnt+=a[i]==c?1:0; 
		if(now[q]==c) cnt+=r-(q-1)*len;
		else if(now[q]==-1) for(int i=(q-1)*len+1;i<=r;i++) cnt+=a[i]==c?1:0;
		return cnt;
	}
}
void change(int l,int r,int c){
	int p=pos[l],q=pos[r];
	if(p==q){
		//在同一块内暴力更新
		if(now[p]==-1){
			//该块内的数不是统一的,就直接更新。
			for(int i=l;i<=r;i++) a[i]=c;
		}
		else if(now[p]!=c){
			//如果该块有标记且不是目标修改值,把块内其他元素赋值为标记,再把要被修改的地方进行修改
			for(int i=(p-1)*len+1;i<=p*len;i++) a[i]=now[p];
			now[p]=-1;
			for(int i=l;i<=r;i++) a[i]=c;	
		}	
		//如果本来这一块的标记就是目标修改值,就没必要进行任何操作。
		//否则,由于块内部分元素被修改,该块的标记打为-1。
	}
	else{
		for(int i=p+1;i<=q-1;i++) now[i]=c;
		//修改被全覆盖的块
		//接下来的操作以上面的相同,都是对没有被完全覆盖的块进行操作。
		if(now[p]==-1){
			for(int i=l;i<=p*len;i++) a[i]=c;
		}
		else if(now[p]!=c){
			for(int i=(p-1)*len+1;i<=p*len;i++) a[i]=now[p];
			now[p]=-1;
			for(int i=l;i<=p*len;i++) a[i]=c;	
		}
		if(now[q]==-1){
			for(int i=(q-1)*len+1;i<=r;i++) a[i]=c;
		}
		else if(now[q]!=c){
			for(int i=(q-1)*len+1;i<=q*len;i++) a[i]=now[q];
			now[q]=-1;
			for(int i=(q-1)*len+1;i<=r;i++) a[i]=c;	
		}
		
	}
}
int main(){
	scanf("%d",&n);m=n;len=sqrt(n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		pos[i]=(i-1)/len+1;
	} 
	for(int i=1;i<=pos[n];i++) now[i]=-1;
	//这里巧用了第n个位置的所在块编号,避免用整除判断来计算总块数。
	int l,r,c;
	while(m--){
		scanf("%d%d%d",&l,&r,&c);
		printf("%d\n",get(l,r,c));
		change(l,r,c);
	}
	return 0;
} 

有些题目要统计的数值很特殊,所以需要拓展标记数组。

这里以蒲公英为例。

如果题目不强制在线,那么可以用莫队处理,但是不能用莫队就只能用分块。

这道题需要求区间众数,而区间众数不具有可加性,所以我们这样考虑:

1.先统计处每个块内数字出现的次数。

2.再预处理出任意个连续块中的众数。

3.询问时,先给出预处理出的完整块的答案,接着枚举左右未完全覆盖区间的数,更新众数。

#include<bits/stdc++.h>
using namespace std;
int n,m;
int len;
int a[101000];
int pos[101000];
int val[101000],cnt;
int qian[400][101000];
int num[400][400];
int cun[101000];
void lisan(){
	//注意离散化
	sort(val+1,val+1+n);
	cnt=unique(val+1,val+1+n)-val-1;
	for(int i=1;i<=n;i++) a[i]=lower_bound(val+1,val+cnt+1,a[i])-val;
}
int main(){
	scanf("%d%d",&n,&m);
	len=sqrt(n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos[i]=(i-1)/len+1,val[i]=a[i];
	lisan();
	for(int i=1;i<=pos[n];i++) {
		for(int j=(i-1)*len+1;j<=min(i*len,n);j++) 
		//1.统计
			qian[i][a[j]]++;
		for(int j=1;j<=cnt;j++)
		//2.预处理
			qian[i][j]+=qian[i-1][j];
	}
	for(int i=1;i<=pos[n];i++){
		for(int j=i;j<=pos[n];j++){
			int minn=num[i][j-1];
			for(int k=(j-1)*len+1;k<=min(n,len*j);k++){
				if((qian[j][a[k]]-qian[i-1][a[k]]>qian[j][minn]-qian[i-1][minn])||(qian[j][a[k]]-qian[i-1][a[k]]==qian[j][minn]-qian[i-1][minn]&&a[k]<minn)) minn=a[k];
			}
			num[i][j]=minn;
		}
	}
	int mod=0;
	while(m--){
		int ll,rr;
		scanf("%d%d",&ll,&rr);
		ll=(ll+mod-1)%n+1,rr=(rr+mod-1)%n+1;
		if(ll>rr) swap(ll,rr);
		int p=pos[ll],q=pos[rr];
		int minn=0;
		if(q-p<=1) {
			for(int i=ll;i<=rr;i++) cun[a[i]]++;
			for(int i=ll;i<=rr;i++)	if(cun[a[i]]>cun[minn]||(cun[a[i]]==cun[minn]&&a[i]<minn)) minn=a[i];
			for(int i=ll;i<=rr;i++) cun[a[i]]=0;
		}
		else {
			for(int i=ll;i<=len*p;i++) cun[a[i]]++;
			for(int i=len*(q-1)+1;i<=rr;i++) cun[a[i]]++;
			minn=num[p+1][q-1];
			for(int i=ll;i<=len*p;i++){
				int k=cun[minn]+qian[q-1][minn]-qian[p][minn],now=cun[a[i]]+qian[q-1][a[i]]-qian[p][a[i]];
				if(now>k||(now==k&&minn>a[i])) minn=a[i];
			}
			for(int i=len*(q-1)+1;i<=rr;i++)
			{
				int k=cun[minn]+qian[q-1][minn]-qian[p][minn],now=cun[a[i]]+qian[q-1][a[i]]-qian[p][a[i]];
				if(now>k||(now==k&&minn>a[i])) minn=a[i];
			}
			for(int i=ll;i<=len*p;i++) cun[a[i]]=0;
			for(int i=len*(q-1)+1;i<=rr;i++) cun[a[i]]=0;
		}
		printf("%d\n",val[minn]);
		mod=val[minn];
	}
	return 0;
}

通过这几道例题不难发现分块是有一定模板的,这是因为分块的思路都是如下这样的:

1.确定要统计的信息的性质。

2.针对性质设计标记。

3.设计转移以及对于未完全覆盖块的处理。

4.注意事项

1.访问到最后一块 x x x时,注意右端点为 m i n ( x ∗ l e n , n ) min(x*len,n) min(xlen,n)

2.记得数组开大一点。

3.做区间混合运算注意优先级。

5.拓展:莫队算法

1.什么是莫队

如果说分块算法是把待询问区间分块,那么莫队算法则是把所有的询问分块。

我们把所有的询问离线后,按照端点所在块排序,从左到右,依次把已经答案的区间转移,这样的时间复杂度 O ( Q ∗ ( N ) ) O(Q * \sqrt(N)) O(Q( N))

不难看出,能用莫队的题必须具有的特性是询问可离线,强制在线的题不能用莫队,所以也是有一定局限性的。

2.莫队的一般步骤

HH的项链为例

显然暴力去询问只会得到香葱炒鸡蛋,所以必须上莫队。

#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int pos[501000];
void pre(){
	for(int i=1;i<=n;i++) pos[i]=i%m==0?i/m:i/m+1;
}
int a[501000];
int b[501000],tot=0;
int c[501000];
void lisan(){
	//记得离散化
	sort(c+1,c+n+1);
	for(int i=1;i<=n;i++)
		if(i==1||c[i]!=c[i-1]) 
			b[++tot]=c[i];
	for(int i=1;i<=n;i++)
	a[i]=lower_bound(b+1,b+tot+1,a[i])-b;
}
struct node{
	int l,r,id;
}ask[501000];
int now=0;
bool mycmp(node x,node y){
	return (pos[x.l]<pos[y.l])||(pos[x.l]==pos[y.l]&&pos[x.r]<pos[y.r]);
	//按照左右端点所在块编号排序
}
int num[501000];
void add(int id){
	num[a[id]]++;
	if(num[a[id]]==1) now++;
}
void del(int id){
	num[a[id]]--;
	if(num[a[id]]==0) now--;
}
int ans[501000];
int main(){
	scanf("%d",&n);
	m=sqrt(n);pre();
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),c[i]=a[i];
	lisan();
	scanf("%d",&q);
	for(int i=1;i<=q;i++) scanf("%d%d",&ask[i].l,&ask[i].r),ask[i].id=i;
	sort(ask+1,ask+1+q,mycmp);
	for(int i=1;i<=n;i++) {
		num[a[i]]++;
		if(num[a[i]]==1) now++; 
		//相当于1~n的add操作
	}
	int ll=1,rr=n;
	for(int i=1;i<=q;i++) {
		//移动当前选择区间的左右端点,并增删元素
		while(ll<ask[i].l) del(ll),ll++;
		while(ask[i].l<ll) ll--,add(ll);
		while(rr>ask[i].r) del(rr),rr--;
		while(ask[i].r>rr) rr++,add(rr);
		ans[ask[i].id]=now;
	}
	for(int i=1;i<=q;i++) printf("%d\n",ans[i]);
	return 0;
}

总结下来,莫队的基本步骤:

1.离线询问,按照块编号排序。

2.先统计整个区间全部答案,再进行区间端点的转移,区间元素的增删。

3.按照询问顺序输出答案。

3.注意事项

1.使用莫队必须保证询问可离线。

2.记得移动端点时是否最终的区间包含的端点的元素。

建议格式:

while(ll<ask[i].l) del(ll),ll++;
while(ask[i].l<ll) ll--,add(ll);
while(rr>ask[i].r) del(rr),rr--;
while(ask[i].r>rr) rr++,add(rr);

3.最后输出答案要按照询问顺序排序后输出。

6.例题

loj:分块入门1~9。
传送门:
1 2 3
4 5 6
7 8 9

磁力块(分块+队列)
小Z的袜子 (莫队+gcd)
XOR and Favorite Number (莫队+异或+逆向思维)

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值