线性结构 —— 分块算法

【概述】

所谓分块,即对于一个长度为 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;
}

对于区间查询,有三种情况:

  1. 询问区间 [x,y] 中,x 不是块的左边界,y 也不是块的右边界
  2. 询问区间 [x,y] 中,x 不是块的左边界,y 是块的右边界
  3. 询问区间 [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)(询问区间众数)点击这里
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值