块状数组

Posted 青石巷

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了块状数组相关的知识,希望对你有一定的参考价值。

定义:

块状数组是基于分块思想的数据结构,较基于分治思想的数据结构如线段树、平衡树等效率较低,但通用性更强。在块状数组的基础上加以扩展,就可以得到块状链表。

原理:

普通数组在处理一些区间问题时,复杂度通常会退化至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];
View Code

维护

维护也就是标记下放,首先保证每个块存储的区间信息在任何时候都是正确的,原数组中存储的信息则不一定每时每刻都正确。在对块中部分元素进行处理时需要先将标记下放,保证原数组中对应块的该部分元素正确。记得在标记下放后将标记变为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;
}
View Code

修改

对[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;}
}
View Code

查询

查询与修改相差不大,也是先判断两端点是否在一个块内,如果是则暴力查询,但注意暴力对原数组元素进行查询时应先将标记下放,保证原数组中元素正确。若两端点不在一个块内,则将两端点所在块的部分元素暴力查询(记得下放),然后对中间每个被包含的块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;
}
View Code

至此,我们用分块的思想解决了一类区间问题。

应用:

块状数组可以在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\n",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\n",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\n",j,b[j].l,b[j].r,b[j].s,b[j].d);}
54     }
55     return 0;
56 }
View Code

 

以上是关于块状数组的主要内容,如果未能解决你的问题,请参考以下文章

[NOI2003]Editor(块状链表)

VSCode自定义代码片段—— 数组的响应式方法

VSCode自定义代码片段10—— 数组的响应式方法

渲染2D块状地图 - LibGDX

内联块状元素

内联块状元素说明