1 数列分块入门_【分块】数列分块入门 By Hoedx

数列的操作,我们有很多算法,例如线段树、树状数组、st表等等。它们的时间复杂度都是log级别的。这种高级数据结构是很优美也很实用的。但是我们避不开一个问题,就是内存空间。但是我认为最大的问题还是可实现度,也可以说好不好写。线段树的代码相对而言是比较难写的,占用空间相对较大。在一般数列操作上 ,我们可以尝试引入分块算法。

分块算法

分块,顾名思义就是将数列分成一块一块的来处理来维护。我们可以利用一个例题来讲解分块的原理。例题:给定一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。

假设你有学过树状数组或者线段树,可以知道这是一个最最基础的模板题。那么用分块应该怎么解决这个问题呢?

如果你学过上面两种高级数据结构,请抛开它们。假设你并不知道,你会用什么办法来解决呢?很显然,我们会用暴力的算法。也就是在区间加法的时候一遍遍扫过去更新每一个的值就好了。这种算法肯定是不能AC的,但是我们可以联想一下分块的性质。我们可以把数列分成一个又一个的块,在每一个块来用暴力的方法处理和维护。就可以降低很多的时间复杂度,你可能会觉得疑惑:就这? 是的,所以分块算法又称为“优雅的暴力”。用hzwer学长的话来讲:“数列分块就是把数列中每m个元素打包起来,以达到优化算法的目的。”

我们现在来介绍几个关键名词:块:我们将数列划分成若干个不相交的区间,每个区间称为一个块

整块:在一个区间操作时,完整包含于区间的块

不完整的块:在一个区间操作时,只有部分包含于区间的块,即区间左右端点所在的两个块

以例题为例子,假设我们把m个元素分成一块,那么共有n/m块,那么区间加法的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。

我们可以利用数组先给每一个块设置一个加法标记,也就是记录这一块内的元素一起加了多少。假设所给定的区间包含整块,我们可以直接修改整块的标记,时间复杂度是O(1)。那么假设跨过了不完整的块时,我们可以怎么办呢?答案是:直接暴力修改不完整的块里面的值,因为不完整的块内元素比较少。那么在单点询问时,直接返回元素的值加上加法标记。

这样每次操作的复杂度是O(n/m)+O(m),我们还可以通过设置m的值使得复杂度尽可能的小。根据基本不等式,当m取得√n时总复杂度最低,所以分块算法的时间复杂度我们取到了根号级别。

我们可以通过代码和注释来深刻理解这种算法:

#includeusing namespace std;

const int N=50050;

int n;

struct Block{ //结构体储存函数和变量 int a[N],k,len,L[N],R[N],F[N],add[N];

//变量介绍:a[N]记录数列,k指块的数目,len指块的长度 // L[N]记录每个块起始元素的下标,R[N]记录每个块末尾元素的下标 // F[N]记录每一个元素所属哪一个块,add[N]也就是加法标记 inline void Build(){ //建块 memset(a,0,sizeof(a));

memset(add,0,sizeof(add));

for(int i=1;i<=n;i++)

scanf("%d",&a[i]); //输入原数列 len=sqrt(n); k=n/len; //计算块的长度和数目 if(n%k) k++; //特判最后一个不完整块 for(int i=1;i<=k;i++)

R[i]=i*len,L[i]=R[i-1]+1; //计算每个块起始和末尾元素的下标 R[k]=n; //特判最后一个块的末尾下标为n for(int i=1;i<=k;i++)

for(int j=L[i];j<=R[i];j++)

F[j]=i; //计算每一个元素所属哪一个块 }

inline void Add(int x,int y,int z){ //区间加法 if(F[x]==F[y]){ //如果区间被包含于一个整块 for(int i=x;i<=y;i++) a[i]+=z; //直接上传加法标记 return; //返回即可 }

//如果区间跨过整块 for(int i=x;i<=R[F[x]];i++) a[i]+=z; //把左边的不完整块的元素值直接改变 for(int i=L[F[y]];i<=y;i++) a[i]+=z; //把右边的不完整块的元素值直接改变 for(int i=F[x]+1;i

inline int Ask(int x){ //单点询问 return a[x]+add[F[x]]; //直接输出原数列的值和块的加法标记 }

}B;

signed main(){

scanf("%d",&n);

B.Build(); //建块 for(int i=1,opt,l,r,c;i<=n;i++){

scanf("%d%d%d%d",&opt,&l,&r,&c);

if(!opt) B.Add(l,r,c); //区间加法 else printf("%d\n",B.Ask(r)); //单点询问 }

return 0;

}

分块算法的优越性

分块算法的时间复杂度是根号级别,是比不上线段树、平衡树等的log级别。但是,分块在联赛中的优势,是很明显的。

首先,分块独特的暴力解法保证了它能够维护的东西很多。可以解决很多类的数列操作问题。例如:区间小于/大于某个数的数、区间求和、区间乘法和单点询问、单点插入和单点询问、平均数/众数等等。

而且,有的时候因为线段树巨大的常数,线段树反而没有分块跑得快。

还有,如果一道题目限了很小的空间,线段树或者平衡树是很容易爆空间的。

再者,如果一道题很多方法都能做,你愿意用调一个多小时的线段树做,还是暴力好写的分块呢?

之后的更新会再讲解几道分块的例题。

Hoedx 2020.3.26

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值