分块
这是神奇的分块。
分块是一种基于暴力演变而来的数据结构思想(并非完全是数据结构),其原理非常原始和简单,也比较好写。
怎么说呢,分块其实是线段树的低配版,毕竟它们两个都是块状数据结构,并且都支持单/区的修/询,能维护各种值(譬如max,sum等),甚至在做区修时都可以使用懒标记来提升效率。
分块的基本套路是将一个区间分成几个长度相等的块(最后一个块的块长不一定是之前的块长)
例如:数组a 为
1 1 4 5 1 4 1 9 1 9 8 1 0 8 8 4 8
一共有17个数。
我们按照4个一块将其分为4块。
1 1 4 5
在讲述后述内容前:
我们先声明一些定义:
- 整块 长度为程序原本设定的块长,包括最后那个长度不够的块。
给出公式:
第
第
约定:
数列(区间)长度为
块长(相等)长度为
块长为
啊不是,8848胎核精野兽先辈手坤
问题1
在处理一个区间时,我们将其分为三个区间:
-
左散块:左边不足一个整块的块
-
右散块:右边不足一个整块的块
-
中间整块:在中间的一些整块
比如处理区间3~15,就将其分为:
4 5
中间两个块为中间整块。左右散块分别为"4 5" 和 "0 8 8".
假设我们需要如下操作:
-
把
a[i] 增加x (修改) -
查询
x,y 的最大值(询问)
首先对于修改操作,我们直接修改原数组
然后由于我们的分块是个块状数据结构,我们对于每个整块都用一个数组记录每个块的最大值
所以,当我们修改完了该块时,我们需要修改这个块所属的
然后查询的时候,分别从左右两个散块所在的数列中暴力查找最大值,然后从中间的整块中暴力查找
代码:
void Modify(int x,int y) //将第x个数加y
{
int k=(x-1)/s+1; //k为x所在块块号
int L=(k-1)*s+1; //第k块的左界
int R=k*s; //第k块的右界
Max[k]=-inf;
a[x]+=y;
for(int i=L;i<=R;i++)
if(a[i]>Max[k])Max[k]=a[i];
}
int Query(int x,int y) //询问a数列区间[x,y]的最大值
{
int i=(x-1)/s+1; //i为x所在块块号,s为块长
int j=(y-1)/s+1; //j为y所在块块号
int ans=-inf;
if(i==j) //[x,y]在同一块中,直接暴力枚举
{
for(int k=x;k<=y;k++)
if(ans<a[k])ans=a[k];
}
else
{
for(int k=x;k<=i*s;k++)//左侧部分
if(ans<a[k])ans=a[k];
for(int k=(j-1)*s+1;k<=y;k++)//右侧
if(ans<a[k])ans=a[k];
for(int k=i+1;k<j;k++)//被覆盖的整块
if(ans<Max[k])ans=Max[k];
}
return ans;
}
int main()
{
int i,k,x,y,t,ans;
scanf("%d%d",&n,&m);
s=int(sqrt(n)); //块的大小为根号n
for(i=1;i<=s+1;i++)Max[i]=inf;
for(i=1;i<=n;i++)
{
scanf("%d",&a[i]);
k=(i-1)/s+1; //计算第i个数所在块的块号k
if(a[i]>Max[k])Max[k]=a[i]; //Max[k]记录第k块的最大值
}
for(i=1;i<=m;i++)
{
scanf("%d%d%d",&t,&x,&y);
if(t==1)Modify(x,y);
else printf("%d\n",Query(x,y));
}
}
修改
问题2
那么对于区间修改呢?既然可以用懒标记,那么怎么用?
对于左右两个散块,无论是修改还是查询,我们都直接修改数组本身,然后修改各自散块所属的
例如修改3~11,每个数增加1,此时数组变成
1 1 5 6
然后,我们对于每个块,都像
在查询时,我们可以同上个问题的询问的思路写代码,但是在判定时需要算上
当然,如果修改时发现左右端点在同一个块内,按照这个思路,程序会把整个块(只有左右散块所以没有中间整块)修改,故大概率会出锅(怎么又是锅啊),所以需要特判。
代码:
void Modify(int x,int y,int z) //将a数列的区间[x,y]整体增加z
{
int i=(x-1)/s+1; int j=(y-1)/s+1; //计算x和y所在块块号,s为块长
if(i==j) //[x,y]在同一块中
{
for(int k=x; k<=y; k++)
{
a[k]+=z;
if(a[k]>Max[i])Max[i]=a[k];
}
}
else
{
for(int k=x; k<=i*s; k++) //左侧散块
{
a[k]+=z;
if(a[k]>Max[i])Max[i]=a[k];
}
for(int k=(j-1)*s+1; k<=y; k++) //右侧散块
{
a[k]+=z;
if(a[k]>Max[j])Max[j]=a[k];
}
for(int k=i+1;k<j;k++) //被覆盖的整块
{
Lazy[k]+=z;
}
}
}
int Query(int x,int y) //询问a数列的区间[x,y]的最大值
{
int i=(x-1)/s+1; int j=(y-1)/s+1; //计算x和y所在块块号,s为块长
int ans=inf;
if(i==j) //[x,y]在同一块中
{
for(int k=x; k<=y; k++)
{
if(a[k]+Lazy[i]>ans)ans=a[k]+Lazy[i];
}
}
else
{
for(int k=x; k<=i*s; k++) //左侧散块
if(a[k]+Lazy[i]>ans)ans=a[k]+Lazy[i];
for(int k=(j-1)*s+1; k<=y; k++) //右侧散块
if(a[k]+Lazy[j]>ans)ans=a[k]+Lazy[j];
for(int k=i+1;k<j;k++) //中间的整块
if(Max[k]+Lazy[k]>ans)ans=Max[k]+Lazy[k];
}
return ans;
}
时间复杂度
求和操作同理。故暂不多谈。
那么有些关于分块的问题。
- 既然是线段树的低配版,为何不写线段树呢?
其实这个问题很古德。
虽然线段树的复杂度是
“一个优秀的选手,在大部分OI赛制的、排名不计运行时间的考试里,只要不TLE,往往需要使用代码运行时间换取编程时间。”
提取
所以我们要让
故当