分块之基本思想
基本思想
树状数组和线段树虽然非常方便,但是它们维护的信息都必须满足信息合并特性(区间可加、可减),若补满足此特性,则不能用树状数组和线段树来做。分块算法可以维护一些线段树维护不了的内容,它其实就是优化过后的暴力算法。分块算法可以解决几乎所有区间更新和区间查询问题,但是效率相对于树状数组和线段树要差一些。
分块算法是将所有数据都分成若干个块,维护块内信息,使得块内查询为 O ( 1 ) O(1) O(1)的时间,而总询问可以被看作若干块询问的总和。
分块算法将长度为
n
n
n的序列分成若干个块,每一块都有
t
t
t个元素,最后一块可能少于
t
t
t个元素。为了使时间复杂度均摊,通常将块的大小设定为
t
=
n
t=\sqrt n
t=n。用pos[i]
来表示第
i
i
i个位置所属的块,对每个块都进行信息维护。
分开算法可以解决以下问题:
- 单点更新:一般先将对应块的懒标记下传,再暴力更新块的状态,时间复杂度为 O ( n ) O(\sqrt n) O(n)
- 区间更新:如果更新的区间横跨若干个块,则只需要对中间完全被覆盖的块打上懒标记,然后对两端剩余部分暴力更新其所属块的状态。每次更新都最多遍历 n \sqrt n n个块,遍历每个块的时间复杂度都是 O ( 1 ) O(1) O(1),两端的两个块暴力更新 n \sqrt n n次。总的时间复杂度是 O ( n ) O(\sqrt n) O(n)
- 区间修改:和区间更新类似,对中间跨过的整个块直接利用块存储的信息统计答案,对两端剩余的部分可以暴力扫描统计。总的时间复杂度是 O ( n ) O(\sqrt n) O(n)
将整个序列分成若干块后进行修改或查询时,对完全覆盖的块直接进行修改,像线段树一样标记或累加;对两端剩余的部分进行暴力修改。分块算法遵循 大段维护,局部朴素 的原则。
预处理
(1)将序列分块,然后将每个块都标记左右端点 L [ i ] L[i] L[i]和 R [ i ] R[i] R[i],对最后一块的右端点需要特别处理。
如下图所示, n = 10 n=10 n=10, t = n = 3 t=\sqrt n=3 t=n=3,每3个元素为一块,那么已经处理了9个元素。但是第10个元素还没有自己的块,因此需要新开一个块来存放第10个元素。因此总共有 n u m = 4 num=4 num=4个块
算法代码:
int t= sqrt(n*1.0); //t表示每个块内的长度(即元素个数)
int num=n/t; //num记录的是有多少个块
//这里处理的是最后剩余的那一小部分 让它自己成为独立的一块
//比如n=10,t=3,那么分成n/t=10/3=3块后,我们发现第10个并不属于前面的3块
//因此它必须新开一个块
if(n%t)
num++;
//分别处理这num个块的左端点和右端点
for(int i=1;i<=num;i++) //i遍历的是块号
{
L[i]=(i-1)*t+1; //第i块的左端点
R[i]=i*t; //第i块的右端点
}
//这里要注意 当上面处理完第num块时 R[num]是按照长度为t进行分配的右端点
//但是这个右端点有可能比n还大 然而我们只需要处理到n就可以了
//因此这里还要特殊处理最后一段的右端点 取到n即可 不一定取最后一段分配到的右端点
R[num]=n;
(2)用pos[]
标记每个元素所属的块,用sum[]
累加每一块的总和
如下图所示:
算法代码:
//遍历这num个块 预处理出下标j是属于哪个块 同时预处理出这个块内的总和
for(int i=1;i<=num;i++) //i遍历的是块
{
for(int j=L[i];j<=R[i];j++)//j遍历的是这个块的元素
{
pos[j]=i; //下标j所对应的元素应该属于块号i
sum[i]+=a[j]; //记录第i块的元素总和
}
}
预处理的算法代码:
//预处理
void init()
{
int t= sqrt(n*1.0); //t表示每个块内的长度(即元素个数)
int num=n/t; //num记录的是有多少个块
//这里处理的是最后剩余的那一小部分 让它自己成为独立的一块
//比如n=10,t=3,那么分成n/t=10/3=3块后,我们发现第10个并不属于前面的3块
//因此它必须新开一个块
if(n%t)
num++;
//分别处理这num个块的左端点和右端点
for(int i=1;i<=num;i++) //i遍历的是块号
{
L[i]=(i-1)*t+1; //第i块的左端点
R[i]=i*t; //第i块的右端点
}
//这里要注意 当上面处理完第num块时 R[num]是按照长度为t进行分配的右端点
//但是这个右端点有可能比n还大 然而我们只需要处理到n就可以了
//因此这里还要特殊处理最后一段的右端点 取到n即可 不一定取最后一段分配到的右端点
R[num]=n;
//遍历这num个块 预处理出下标j是属于哪个块 同时预处理出这个块内的总和
for(int i=1;i<=num;i++) //i遍历的是块
{
for(int j=L[i];j<=R[i];j++)//j遍历的是这个块的元素
{
pos[j]=i; //下标j所对应的元素应该属于块号i
sum[i]+=a[j]; //记录第i块的元素总和
}
}
}
区间更新
区间更新,例如将区间 [ l , r ] [l,r] [l,r]内的元素都加上 d d d
- 求 l l l和 r r r所属的块,即 p = p o s [ l ] p=pos[l] p=pos[l], q = p o s [ r ] q=pos[r] q=pos[r]
- 如果属于同一个块( p = q p=q p=q),则对该区间的元素进行暴力修改,同时更新该块的和值
- 如果不属于同一个块,则对中间被完全覆盖的块都打上懒标记, a d d [ i ] + = d add[i]+=d add[i]+=d,对首尾两端的剩余部分进行暴力修改。
例如下图,将区间 [ 3 , 8 ] [3,8] [3,8]内的元素都加上5,操作过程:
- 读取 3 3 3和 8 8 8所属的块, p = p o s [ 3 ] = 1 p=pos[3]=1 p=pos[3]=1, q = p o s [ 8 ] = 3 q=pos[8]=3 q=pos[8]=3
- 不属于同一个块,中间完整块 [ p + 1 , q − 1 ] [p+1,q-1] [p+1,q−1]为第2块,为该块打上懒标记 a d d [ 2 ] + = 5 add[2]+=5 add[2]+=5
- 对首尾两端剩余部分的元素(下标3,7,8)进行暴力修改,并修改和值
算法代码:
//区间修改 将区间[l,r]中的元素都+d
void change(int l,int r,int d)
{
int p=pos[l]; //获取下标l所在的块号p
int q=pos[r]; //获取下标r所在的块号q
//如果区间[l,r]在同一个块内
if(p==q)
{
//直接将区间[l,r]内的所有元素都+d
for(int i=l;i<=r;i++)
a[i]+=d;
sum[p]+=(r-l+1)*d; //记录第p块内的所有元素的总和
}
//否则说明区间[l,r]跨越了不同的块
else
{
//先处理中间被完全覆盖的块 让这些块的懒标记都+d
for(int i=p+1;i<=q-1;i++)//i枚举的中间被完全覆盖的块号
add[i]+=d;
//处理左边剩余部分 将区间[l,R[p]]内的所有元素都+d
for(int i=l;i<=R[p];i++)
a[i]+=d;
sum[p]+=(R[p]-l+1)*d; //记录左边剩余部分的所有元素的总和
//处理右边剩余部分 将区间[L[q],r]内的所有元素都+d
for(int i=L[q];i<=r;i++)
a[i]+=d;
sum[q]+=(r-L[q]+1)*d; //记录右边剩余部分的所有元素的总和
}
}
区间查询
区间查询,例如查询区间 [ l , r ] [l,r] [l,r]内的元素和值
- 求 l l l和 r r r所属的块,即 p = p o s [ l ] p=pos[l] p=pos[l], q = p o s [ r ] q=pos[r] q=pos[r]
- 如果属于同一个块( p = q p=q p=q),则对该区间的元素进行暴力累加,然后加上该块的懒标记
- 如果不属于同一个块,则对中间被完全覆盖的块累加
sum[]
值和懒标记add[]
上的值,然后对首尾两端剩余部分暴力累加其元素及懒标记的值
如下图,查询区间 [ 2 , 7 ] [2,7] [2,7]的元素和值,操作过程:
- 读取 2 2 2和 7 7 7所属的块, p = p o s [ 2 ] = 1 p=pos[2]=1 p=pos[2]=1, q = p o s [ 7 ] = 3 q=pos[7]=3 q=pos[7]=3
- 不属于同一个块,中间完整块 [ p + 1 , q − 1 ] [p+1,q-1] [p+1,q−1]为第2块,累加这些块的懒标记, a n s = s u m [ 2 ] + ( R [ 2 ] − L [ 2 ] + 1 ) × a d d [ 2 ] = 42 + 5 × 3 = 57 ans=sum[2]+(R[2]-L[2]+1)\times add[2]=42+5\times3=57 ans=sum[2]+(R[2]−L[2]+1)×add[2]=42+5×3=57
- 对首尾两端剩余部分的元素暴力累加元素值及懒标记值,此时懒标记 a d d [ 1 ] = a d d [ 3 ] = 0 add[1]=add[3]=0 add[1]=add[3]=0, a n s + = 5 + 7 + 9 + ( R [ p ] − l + 1 ) × a d d [ p ] + ( r − L [ q ] + 1 ) × a d d [ q ] = 5 + 7 + 9 + ( 3 − 2 + 1 ) × a d d [ 1 ] + ( 7 − 7 + 1 ) × a d d [ 3 ] = 78 ans+=5+7+9+(R[p]-l+1)\times add[p]+(r-L[q]+1)\times add[q]=5+7+9+(3-2+1)\times add[1]+(7-7+1)\times add[3]=78 ans+=5+7+9+(R[p]−l+1)×add[p]+(r−L[q]+1)×add[q]=5+7+9+(3−2+1)×add[1]+(7−7+1)×add[3]=78
算法代码:
//区间查询
LL query(int l,int r)
{
int p=pos[l]; //获取下标l所在的块号p
int q=pos[r]; //获取下标r所在的块号q
LL ans=0; //记录区间[l,r]中的元素总和
//如果区间[l,r]在同一个块内
if(p==q)
{
//累加这个区间[l,r]中所有元素的总和
for(int i=l;i<=r;i++)
ans+=a[i];
ans+=(r-l+1)*add[p]; //同时要记得加上第p块中的懒标记
}
//否则说明区间[l,r]跨越了不同的块
else
{
//先累加中间被完全覆盖的这些块的懒标记
for(int i=p+1;i<=q-1;i++) //i遍历的是中间被完全覆盖的这些块的块号
ans+=sum[i]+(R[i]-L[i]+1)*add[i];
//累加左边剩余部分的所有元素的总和
for(int i=l;i<=R[p];i++)
ans+=a[i];
//累加左侧剩余部分的第p块中的懒标记
ans+=(R[p]-l+1)*add[p];
//累加左=右边剩余部分的所有元素的总和
for(int i=L[q];i<=r;i++)
ans+=a[i];
//累加右侧剩余部分的第q块中的懒标记
ans+=(r-L[q]+1)*add[q];
}
return ans; //返回答案
}