分块之基本思想

分块之基本思想


基本思想

树状数组和线段树虽然非常方便,但是它们维护的信息都必须满足信息合并特性(区间可加、可减),若补满足此特性,则不能用树状数组和线段树来做。分块算法可以维护一些线段树维护不了的内容,它其实就是优化过后的暴力算法。分块算法可以解决几乎所有区间更新和区间查询问题,但是效率相对于树状数组和线段树要差一些。

分块算法是将所有数据都分成若干个块,维护块内信息,使得块内查询为 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个块

image-20210816203458800

算法代码:

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[]累加每一块的总和

如下图所示:

image-20210816203520043

算法代码:

    //遍历这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

  1. 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]
  2. 如果属于同一个块( p = q p=q p=q),则对该区间的元素进行暴力修改,同时更新该块的和值
  3. 如果不属于同一个块,则对中间被完全覆盖的块都打上懒标记, a d d [ i ] + = d add[i]+=d add[i]+=d,对首尾两端的剩余部分进行暴力修改。

例如下图,将区间 [ 3 , 8 ] [3,8] [3,8]内的元素都加上5,操作过程:

image-20210816203544411

  • 读取 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,q1]为第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]内的元素和值

  1. 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]
  2. 如果属于同一个块( p = q p=q p=q),则对该区间的元素进行暴力累加,然后加上该块的懒标记
  3. 如果不属于同一个块,则对中间被完全覆盖的块累加sum[]值和懒标记add[]上的值,然后对首尾两端剩余部分暴力累加其元素及懒标记的值

如下图,查询区间 [ 2 , 7 ] [2,7] [2,7]的元素和值,操作过程:

image-20210816203607676

  • 读取 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,q1]为第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]+(rL[q]+1)×add[q]=5+7+9+(32+1)×add[1]+(77+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; //返回答案
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值