0x00 写在前面
新年假期想学点东西,本着简单的想法,找了个感觉最简单的分块来学
近期目标:数列分块入门 尽量去做,另外做一些蓝到紫难度的题
0x01 分块是什么&&可以做什么
分块,顾名思义,就是把一个要维护的序列分成几个块来处理,要查询区间信息,就把区间拆分到块里,从而通过提取块内的信息达到降低时间复杂度的目的
做一个形象的比喻,科任老师要清作业,如果直接把全班的作业交上来数肯定会很累,于是科任老师分了个小组,小组长负责清出本组有多少人交,科任老师就只需要向每个小组长去询问,时间大大减少
0x02 分块的操作
分块的操作一般是维护和查询,一个对于 [ l , r ] [l,r] [l,r]的操作,对于在该区间内的完整的块,我们直接维护整个块的信息;对于两侧不完整的部分,我们暴力修改每个点的信息。写分块的关键就是想好如何维护和查询。
考虑到上面的操作方法,分块一般是以 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(