• 块状数组


    定义:

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

    原理:

    普通数组在处理一些区间问题时,复杂度通常会退化至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
    ",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 }
    View Code
  • 相关阅读:
    php学习推荐
    python 安装numpy报错
    PHP最基础
    php自定义错误函数
    phpMyAdmin安装
    php链接mysql提示:Call to undefined function mysql_connect()
    POJ 1005 解题报告
    亚马逊在线笔试(2014/10/9)
    LeetCode 题目 word break 思路剖析与改进
    Dijkstra单源最短路算法的C++实现
  • 原文地址:https://www.cnblogs.com/XSC637/p/7467192.html
Copyright © 2020-2023  润新知