线性结构 —— 分块算法
【概述】
所谓分块,即对于一个长度为 n 的序列,设块的大小为 S,从序列的第一个元素起,每 S 个元素被分成一块,若剩余的元素不足 S 个,令他们组成一块。经过分块后的数组,称为块状数组,在块状数组的基础上加以扩展,即可得到块状链表。
在一个区间操作时,完整包含于区间的块称为整块,只有部分包含于区间的块,称为不完整的块,不完整的块实质上行即为区间左右端点所在的两个块。
在许多区间问题中,使用树型结构,如:树状数组、线段树等会更加优秀,但当所需的某些信息无法快速合并时,就只能使用分块,例如:询问一个区间的众数
需要注意的是,利用来解决的问题,大部分都是要求强制在线的,此外,在时间复杂度上:树状数组 > 线段树 > 块状数组(分块)
但三者各有优劣:
- 树状数组:代码简单、常数小
- 线段树:代码复杂、功能强大、常数大
- 块状数组:代码复杂、常数大、能维护一些难以快速合并的数据
【基本原理】
从时间复杂度来考虑,一般来说,分块长度为 sqrt(n),那么,初始化时有:
int block,sum;//block为块的长度,sum为块的个数
int a[N];//存放数列元素
int pos[N],tag[N];//pos记录第i个元素在第几个块中,tag为操作标记
int ans[N];//维护整块和
void init(){//初始化
block=(int)sqrt(n) //块的长度
sum=n/block;//块数
if(n%block)
sum++;
for(int i=1;i<=n;i++)//第i个元素在第几块中
pos[i]=(i-1)/block+1;
}
对于区间查询,有三种情况:
- 询问区间 [x,y] 中,x 不是块的左边界,y 也不是块的右边界
- 询问区间 [x,y] 中,x 不是块的左边界,y 是块的右边界
- 询问区间 [x,y] 中,x 是块的左边界,y 不是块的右边界
由于可以预处理每一块的值,因此对于一块来说,查询的时间复杂度是 O(1) 的,对于多出来的边角料的部分总和不会超过 2*sqrt(n),基于这个时间复杂度,进行暴力求解,可以发现对于第一种情况,q 次查询的复杂度为 O(q*sqrt(n)),对于第二、三中情况,完全可以当成情况 1 来处理
对于一个块的左边界,可以求出上一个块的右边界 (pos[x]-1)*block,那么 +1 后就是这个块的左边界,即:(pos[x]-1)*block+1
因此,在每次询问时,第 i 个元素 a[i] 处于第 (i-1)/sqrt(n)+1 块,其左端点为 (i-1)*sqrt(n)+1,右端点为 min(i*sqrt(n),n),返回元素的值再加上其所在块的标记即可。
int block,sum;//block为块的长度,sum为块的个数
int a[N];//存放数列元素
int pos[N],tag[N];//pos记录第i个元素在第几个块中,tag为操作标记
int ans[N];//维护整块和
int query(int L,int R){
for(int i=L;i<=min(R,pos[L]*block);i++)//处理左边角料
//do something
for(int i=(pos[R]-1)*block+1;i<=R;i++)//处理右边角料
//do something
for(int i=pos[L]+1;i<=pos[R]-1;i++)//处理中间k块
//do something
}
对于修改操作,与查询操作一样,同样分成三部分,对于边角料暴力修改 a[i],对于每一块,修改每一块的标记值 tag[pos[i]],那么,对于 m 次修改,总时间复杂度为 O(m*sqrt(n))
int block,sum;//block为块的长度,sum为块的个数
int a[N];//存放数列元素
int pos[N],tag[N];//pos记录第i个元素在第几个块中,tag为操作标记
void change(int x){
//do something
}
void update(int L,int R,int x){
for(int i=L;i<=min(R,pos[L]*block);i++)//处理左边角料,对a[i]进行操作
change(x);
if(pos[L]!=pos[R])//存在右区间才遍历,防止重复计算
for(int i=(pos[R]-1)*block+1;i<=R;i++)//处理右边角料,对a[i]进行操作
change(x);
for(int i=pos[L]+1;i<=pos[R]-1;i++)//处理中间k块,对tag[i]进行操作
change(x);
}
而对于整块的预处理,即使每个块暴力进行对 ans[i] 的预处理,时间复杂度也只有 O(n)
int ans[N];//维护整块
void work(int L,int R,int x){
int start=0;
for(int i=L;i<=R;i++)
//do something with start
ans[x]=start;//整块的标记
}
int mian(){
...
for(int i=1;i<=sum;i++)//传入块的左右边界与编号
work((i-1)*block+1,i*block,i);
...
}
【模版&例题】
模版原理:分块九讲
- 数列分块入门 1(LibreOj-6277)(区间加法,单点询问):点击这里
- 数列分块入门 2(LibreOj-6278)(区间加法,询问区间内小于某个值 x 的元素个数):点击这里
- 数列分块入门 3(LibreOj-6279)(区间加法,询问区间内小于某个值 x 的前驱):点击这里
- 数列分块入门 4(LibreOj-6280)(区间加法,询问区间和):点击这里
- 数列分块入门 5(LibreOj-6281)(区间开方,询问区间和):点击这里
- 数列分块入门 6(LibreOj-6282)(单点插入,单点询问):点击这里
- 数列分块入门 7(LibreOj-6283)(区间乘法与加法,单点询问):点击这里
- 数列分块入门 8(LibreOj-6284)(询问区间某值个数,区间赋值):点击这里
- 数列分块入门 9(LibreOj-6285)(询问区间众数):点击这里
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】