【总结】分块

0x00 写在前面


新年假期想学点东西,本着简单的想法,找了个感觉最简单的分块来学

近期目标:数列分块入门 尽量去做,另外做一些蓝到紫难度的题

0x01 分块是什么&&可以做什么


分块,顾名思义,就是把一个要维护的序列分成几个块来处理,要查询区间信息,就把区间拆分到块里,从而通过提取块内的信息达到降低时间复杂度的目的

做一个形象的比喻,科任老师要清作业,如果直接把全班的作业交上来数肯定会很累,于是科任老师分了个小组,小组长负责清出本组有多少人交,科任老师就只需要向每个小组长去询问,时间大大减少

0x02 分块的操作


分块的操作一般是维护和查询,一个对于 [ l , r ] [l,r] [lr]的操作,对于在该区间内的完整的块,我们直接维护整个块的信息;对于两侧不完整的部分,我们暴力修改每个点的信息。写分块的关键就是想好如何维护和查询。

考虑到上面的操作方法,分块一般是以 n \sqrt{n} n 长度为一块,分作 n \sqrt{n} n 块,那么上面的时间复杂度就可以达到 n + 2 n \sqrt{n}+2\sqrt{n} n +2n ,比之 O ( n ) O(n) O(n) 的暴力查询要快,但又比线段树,树状数组等数据结构的 O ( l o g n ) O(logn) O(logn) 慢,由于便于理解,且直接在原数组上划分,更加方便,所以是一个即优美的暴力数据结构

0x03 划分(初始化)


上面已经介绍了,我们以 n \sqrt{n} n 为块长,分剩下的就存进后一个块

维护分块,我们需要基本的几个数组在下面,并注释出含义,而根据题意,我们再增加需要维护的东西

const int MAXN=5e4+5;//原序列的最大长度
const int MAXM=3e2+5;//块的个数,同时也是块的大小
int n;//原序列长度
int a[MAXN];//原序列
int l[MAXM];//l[i]表示第i个块的左端点
int r[MAXM];//r[i]表示第i个块的右端点
int pos[MAXN];//pos[i]表示第i个数在第几个块
int size;//每个块的大小
int m;//块的个数

接下来我们就要预处理出上面的这些东西:

代码比较简单,照着上面的划分方法打就行了

void pre(){
   
	size=sqrt(n);
	m=ceil(1.0*n/size);
	for(int i=1;i<=m;i++){
   
		l[i]=r[i-1]+1;
		r[i]=i*size;
	} 
	r[m]=n;
	for(int i=1;i<=n;i++){
   
		pos[i]=(i-1)/dis+1;
	}
}

0x04 例题


是的,你没看错,模板就只有这些,这就是为什么说他简单,接下来就是自己想如何利用这些块去维护出题目需要的东西了。

T1:数列分块入门 1

给出一个长为 n n n 的数列,以及 n n n 个操作,操作涉及区间加法,单点查值。

学过线段树的都知道有一个东西叫做 lazy-tag,它的思想就是笼统的保存一个信息,你只要不仔细来查我下面管辖的信息,我就不去更新他们,这个tag就一直在我这里

用在这道题上,本题就迎刃而解

我们对每一个块,维护一个 lazy标记,查的时候,就加上它所在的块的标记就行了

具体来讲:

  • 对于修改操作,将修改区间内的所有完整的块都加上一个标记,对于不是完整的块,暴力的修改即可

  • 对于查询操作,返回这个数与这个数所在的块的标记之和

实现

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
const int MAXN=5e4+5;
const int MAXM=3e2+5;
int n;
int a[MAXN];
int l[MAXM];
int r[MAXM];
int pos[MAXN];
int lazy[MAXM];
int size;
int m;
void pre(){
   
	size=sqrt(n);
	m=ceil(1.0*n/size);
	for(int i=1;i<=m;i++){
   
		l[i]=r[i-1]+1;
		r[i]=i*size;
	} 
	r[m]=n;
	for(int i=1;i<=n;i++){
   
		pos[i]=(i-1)/size+1;
	}
}
void add(int ql,int qr,int v){
   
	if(pos[ql]==pos[qr]){
   
		for(int i=ql;i<=qr;i++){
   
			a[i]+=v;
		}
	}
	else{
   
		for(int i=ql;i<=r[pos[ql]];i++){
   
			a[i]+=v;
		}
		for(int i=pos[ql]+1;i<=pos[qr]-1;i++){
   
			lazy[i]+=v;
		}
		for(int i=l[pos[qr]];i<=qr;i++){
   
			a[i]+=v;
		}
	}
}
int query(int x){
   
	return a[x]+lazy[pos[x]];
}
int main(){
   
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
   
		scanf("%d",&a[i]);
	}
	pre();
	for(int i=1;i<=n;i++){
   
		int type,l,r,c;
		scanf("%d %d %d %d",&type,&l,&r,&c);
		if(type==0){
   
			add(l,r,c);
		}
		else{
   
			printf("%d\n",query(r));
		}
	}
return 0;
} 

T2:数列分块入门 2

给出一个长为 n n n 的数列,以及 n n n 个操作,操作涉及区间加法,询问区间内小于某个值 x x x 的元素个数。

首先修改仍然呢是和第一题是一样的,有改动的是查询操作

对于这个查询,我们想到的肯定应该是二分,但是二分的一个重要要求就是有序,由于是区间查询,所以我们只能对区间内的元素去排序,要不然这个排序就会把一些错误的东西给换进来,就会有问题,那么不难想到的就是对区间内完整的块给排序,对每一个块进行二分查找,对于不完整的,暴力的枚举即可

考虑到上面的算法涉及两个数组,分别是原数组 a(用来不完整的块的枚举)以及对每个块内排好了序的b(用来对完整的块内进行二分),所以我们再修改操作时需要相应的维护好b数组。
也就是说,如果需要修改的是块的边角,这时b数组的单调性可能会被改变,所以需要暴力的维护这个边角对应的整个块的b
如果需要修改的是整个块,那么整个块都要加上一个相同的数,b的单调性并不会没改变,所以只需要维护好lazy-tag就好

代码:

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=5e4+5;
const int MAXM=3e2+5;
int n;
int a[MAXN];
int l[MAXM];
int r[MAXM];
int pos[MAXN];
int lazy[MAXM];
int b[MAXN];
int size;
int m;
void pre(){
   
	size=sqrt(n);
	m=ceil(1.0*n/size);
	for(int i=1;i<=m;i++){
   
		l[i]=r[i-1]+1;
		r[i]=i*size;
	} 
	r[m]=n;
	for(int i=1;i<=n;i++){
   
		pos[i]=(i-1)/size+1;
	}
	for(int i=1;i<=n;i++){
   
		b[i]=a[i];
	}
	for(int i=1;i<=m;i++){
   
		sort(b+l[i],b+r[i]+1);
	}
}
void add(int ql,int qr,int v){
   
	if(pos[ql]==pos[qr]){
   
		for(int i=ql;i<=qr;i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[ql]];i<=r[pos[ql]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[ql]],b+r[pos[ql]]+1);
	}
	else{
   
		for(int i=ql;i<=r[pos[ql]];i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[ql]];i<=r[pos[ql]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[ql]],b+r[pos[ql]]+1);
		for(int i=pos[ql]+1;i<=pos[qr]-1;i++){
   
			lazy[i]+=v;
		} 
		for(int i=l[pos[qr]];i<=qr;i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[qr]];i<=r[pos[qr]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[qr]],b+r[pos[qr]]+1);
	}
} 
int query(int ql,int qr,int v){
   
	int cnt=0;
	if(pos[ql]==pos[qr]){
   
		for(int i=ql;i<=qr;i++){
   
			if(a[i]+lazy[pos[ql]]<v){
   
				cnt++;
			}
		}
		return cnt;
	}
	for(int i=ql;i<=r[pos[ql]];i++){
   
		if(a[i]+lazy[pos[ql]]<v){
   
			cnt++;
		}
	}
	for(int i=l[pos[qr]];i<=qr;i++)
		if(a[i]<v-lazy[pos[qr]]){
   
			cnt++;
		}
	for(int i=pos[ql]+1;i<=pos[qr]-1;i++){
   
		cnt+=lower_bound(b+l[i],b+r[i]+1,v-lazy[i])-(b+l[i]);
	}
	return cnt;
}

int main(){
   
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
   
		scanf("%d",&a[i]);
	}
	pre();
	for(int i=1;i<=n;i++){
   
		int type,l,r,c;
		scanf("%d %d %d %d",&type,&l,&r,&c);
		if(type==0){
   
			add(l,r,c);
		}
		else{
   
			printf("%d\n",query(l,r,c*c));
		}
	}
return 0;
} 

T3:数列分块入门 3

给出一个长为 n n n 的数列,以及 n n n 个操作,操作涉及区间加法,询问区间内小于某个值 x x x 的前驱(比其小的最大元素)。

这道题和T2很像,修改操作还是老样,查询操作仍然是借助二分查找

修改操作就不说了,和T2一模一样,搬上去就行了

至于查询操作

  • 对于不完整的块,暴力枚举所有元素取最大值
  • 对于完整的块,二分查找最后一个比 x x x 小的值,则这个值即为所求

一个易错点,这道题不知道为什么, n n n 的范围不一样,所以模板照搬上面的题的时候,一定要注意数据范围

代码

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=1e5+5;
const int MAXM=3e3+5;
int n;
int a[MAXN];
int l[MAXM];
int r[MAXM];
int pos[MAXN];
int lazy[MAXM];
int b[MAXN];
int size;
int m;
void pre(){
   
	size=sqrt(n);
	m=ceil(1.0*n/size);
	for(int i=1;i<=m;i++){
   
		l[i]=r[i-1]+1;
		r[i]=i*size;
	} 
	r[m]=n;
	for(int i=1;i<=n;i++){
   
		pos[i]=(i-1)/size+1;
	}
	for(int i=1;i<=n;i++){
   
		b[i]=a[i];
	}
	for(int i=1;i<=m;i++){
   
		sort(b+l[i],b+r[i]+1);
	}
}
void add(int ql,int qr,int v){
   
	if(pos[ql]==pos[qr]){
   
		for(int i=ql;i<=qr;i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[ql]];i<=r[pos[ql]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[ql]],b+r[pos[ql]]+1);
	}
	else{
   
		for(int i=ql;i<=r[pos[ql]];i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[ql]];i<=r[pos[ql]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[ql]],b+r[pos[ql]]+1);
		for(int i=pos[ql]+1;i<=pos[qr]-1;i++){
   
			lazy[i]+=v;
		} 
		for(int i=l[pos[qr]];i<=qr;i++){
   
			a[i]+=v;
		}
		for(int i=l[pos[qr]];i<=r[pos[qr]];i++){
   
			b[i]=a[i];
		}
		sort(b+l[pos[qr]],b+r[pos[qr]]+1);
	}
} 
int query(int ql,int qr,int v){
   
	int maxn=-1;
	if(pos[ql]==pos[qr]){
   
		for(
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值