定义:
块状数组是基于分块思想的数据结构,较基于分治思想的数据结构如线段树、平衡树等效率较低,但通用性更强。在块状数组的基础上加以扩展,就可以得到块状链表。
原理:
普通数组在处理一些区间问题时,复杂度通常会退化至O(n)。一个朴素的想法就是将这个数组分为若干个子区间,同时维护这些子区间的统计值,如区间和、区间最值等。对于某个子区间,如果操作区间覆盖子区间,则在整体上进行修改并打标记。如果操作区间部分覆盖子区间,则将该块标记下放,对区间中被覆盖部分的元素进行暴力操作。
设数组长度为n,将其分为s块,每块c个元素,那么一次操作最坏情况是遍历所有块,复杂度为O(s)。对于不需暴力处理的块,处理的复杂度为O(1)。易知需要暴力处理的块最多只有2个,而暴力处理一个块的最坏情况是遍历块中几乎所有元素,复杂度为O(c)。总复杂度为O(s+c)。根据s*c=n,由基本不等式知,当s,c取近似n½ 时,总复杂度最小,为O(n½)。这就是分块的思想。对这个算法进行分析后会发现,我们对元素的处理方法本质上还是暴力,只是通过一些数学方法使暴力的复杂度降到了我们可以接受的范围。因此分块思想又被称为“根号的暴力”。
下面给出本人块状数组的写法,以区间和为例。
分块
定义一个结构体数组,数组元素个数为块的个数。数组里存储每一个块的左右端点,以及区间信息和标记信息。
同时保留原数组,再额外定义一个数组belong,belong[i]里存储i所属的块的下标。
#define LL long long struct block{int l,r;LL s,d;}b[maxm]; LL a[maxn],belong[maxn],sum[maxn];
维护
维护也就是标记下放,首先保证每个块存储的区间信息在任何时候都是正确的,原数组中存储的信息则不一定每时每刻都正确。在对块中部分元素进行处理时需要先将标记下放,保证原数组中对应块的该部分元素正确。记得在标记下放后将标记变为0。
void maintain(int x) { int i; if(!b[x].d){return;} for(i=b[x].l;i<=b[x].r;i++){a[i]+=b[x].d;} b[x].d=0; }
修改
对[l,r]的元素进行修改时,先判断两端点是否在一个块内,如果是则暴力修改,在修改原数组中元素的同时也修改块中存储的区间信息,复杂度最坏为O(n½ )。否则先将两个端点所在块的部分元素进行处理,然后对中间被整个覆盖的块修改区间信息,同时打上标记。
void add(int sj,int tj,int dlt) { int i,x=belong[sj],y=belong[tj]; if(x==y){b[x].s+=(tj-sj+1)*dlt;for(i=sj;i<=tj;i++){a[i]+=dlt;}return;} b[x].s+=(b[x].r-sj+1)*dlt;for(i=sj;i<=b[x].r;i++){a[i]+=dlt;} b[y].s+=(tj-b[y].l+1)*dlt;for(i=b[y].l;i<=tj;i++){a[i]+=dlt;} for(i=x+1;i<=y-1;i++){b[i].s+=c*dlt;b[i].d+=dlt;} }
查询
查询与修改相差不大,也是先判断两端点是否在一个块内,如果是则暴力查询,但注意暴力对原数组元素进行查询时应先将标记下放,保证原数组中元素正确。若两端点不在一个块内,则将两端点所在块的部分元素暴力查询(记得下放),然后对中间每个被包含的块O(1)查询。
LL query(int sj,int tj) { int i,j,x=belong[sj],y=belong[tj];LL ans=0; if(x==y){maintain(x);for(i=sj;i<=tj;i++){ans+=a[i];}return ans;} maintain(x);for(i=sj;i<=b[x].r;i++){ans+=a[i];} maintain(y);for(i=b[y].l;i<=tj;i++){ans+=a[i];} for(i=x+1;i<=y-1;i++){ans+=b[i].s;} return ans; }
至此,我们用分块的思想解决了一类区间问题。
应用:
块状数组可以在O(n1.5 )的时间内解决所有普通线段树均能解决的区间问题,且思路简单,易于编写,区别于普通线段树使用的自顶向下的递归形式,常数较小。
由于线段树,树状数组等O(nlogn)的数据结构基于分治的思想,即[l,r]的信息可以通过[l,mid]和[mid+1,r]两个小区间的信息O(1)合并得到,在维护某些不能O(1)合并的信息时,如区间众数,O(nlogn)的数据结构就有心无力了。此时将块状数组进行一些修改,就可以完成这个任务了。其实是不会写这个题
此外,普通线段树无法处理对序列“伤筋动骨”的改变。例如在中间插入或删除几个元素,或者将一段区间翻转,线段树是不支持的。除了splay之外,如果将各个块连接的方式由数组改为链表,就变成了块状链表,可以用相似的方法处理。其实是不会写这个数据结构
例题:
Luogu P3372 【模板】线段树 1 题目链接
题意:写一种数据结构维护一个长度为n的序列的区间和,支持m次区间加减与区间查询。保证结果在long long范围内。
数据范围:n,m≤100000。
题解:普通线段树、扩展后的树状数组、块状数组均可以直接处理这个问题。
代码:
1 #include<bits/stdc++.h> 2 #define LL long long 3 using namespace std; 4 const int maxn=1e5+10,maxm=1e3+10; 5 struct block{int l,r;LL s,d;}b[maxm]; 6 LL a[maxn],belong[maxn],sum[maxn]; 7 int n,m,c,len; 8 void maintain(int x) 9 { 10 int i; 11 if(!b[x].d){return;} 12 for(i=b[x].l;i<=b[x].r;i++){a[i]+=b[x].d;} 13 b[x].d=0; 14 } 15 void build(int n) 16 { 17 int i; 18 c=(int)sqrt(n); 19 len=n/c;if(n%c){len++;} 20 for(i=1;i<=len;i++){b[i].l=1+(i-1)*c;b[i].r=i*c;b[i].s=sum[min(b[i].r,n)]-sum[b[i].l-1];} 21 for(i=1;i<=n;i++){belong[i]=(i-1)/c+1;} 22 } 23 void add(int sj,int tj,int dlt) 24 { 25 int i,x=belong[sj],y=belong[tj]; 26 if(x==y){b[x].s+=(tj-sj+1)*dlt;for(i=sj;i<=tj;i++){a[i]+=dlt;}return;} 27 b[x].s+=(b[x].r-sj+1)*dlt;for(i=sj;i<=b[x].r;i++){a[i]+=dlt;} 28 b[y].s+=(tj-b[y].l+1)*dlt;for(i=b[y].l;i<=tj;i++){a[i]+=dlt;} 29 for(i=x+1;i<=y-1;i++){b[i].s+=c*dlt;b[i].d+=dlt;} 30 } 31 LL query(int sj,int tj) 32 { 33 int i,j,x=belong[sj],y=belong[tj];LL ans=0; 34 if(x==y){maintain(x);for(i=sj;i<=tj;i++){ans+=a[i];}return ans;} 35 maintain(x);for(i=sj;i<=b[x].r;i++){ans+=a[i];} 36 maintain(y);for(i=b[y].l;i<=tj;i++){ans+=a[i];} 37 for(i=x+1;i<=y-1;i++){ans+=b[i].s;} 38 return ans; 39 } 40 int main() 41 { 42 int i,j,flag,x,y;LL k; 43 cin>>n>>m; 44 for(i=1;i<=n;i++){scanf("%lld",&a[i]);sum[i]=sum[i-1]+a[i];} 45 build(n); 46 //printf("c=%d ",c); 47 for(i=1;i<=m;i++) 48 { 49 scanf("%d%d%d",&flag,&x,&y); 50 if(flag==1){scanf("%lld",&k);add(x,y,k);} 51 else{printf("%lld ",query(x,y));} 52 //printf("a[]=");for(j=1;j<=n;j++){printf("%lld ",a[j]);}cout<<endl; 53 //for(j=1;j<=len;j++){printf("i=%d l=%d r=%d s=%lld d=%lld ",j,b[j].l,b[j].r,b[j].s,b[j].d);} 54 } 55 return 0; 56 }