分块算法详解

长为nnn的数列,操作涉及区间修改和区间查询,应该如何处理?
用线段树等许多数据结构都可以做,但是有时用其它数据结构来解决会十分棘手,代码量也会变得很大。这时,我们就需要用到分块了。

分块

分块是一种非常暴力的数据结构。对于一个长度为nnn的序列,将序列分成⌈n⌉\lceil\sqrt n\rceiln块,每块长度最多为⌊n⌋\lfloor\sqrt n\rfloorn,维护每一块的信息,我们设第iii个块的左端点为LiL_iLi,右端点为RiR_iRi

单点查询直接查询就可以了,是O(1)O(1)O(1)的。单点修改需要修改点和该点所在的区间,一般情况下也可以达到O(1)O(1)O(1)

接下来就是区间查询和区间修改了。比如要将区间[l,r][l,r][l,r]进行区间查询,先找到lll所在的块aaarrr所在的块bbb。对[l,Ra][l,R_a][l,Ra][Lb,r][L_b,r][Lb,r]暴力查询,然后在第a+1a+1a+1b−1b-1b1块上打懒标记或每块逐个处理即可。区间修改与区间查询类似。

对于单次操作,枚举两边多余部分O(n)O(\sqrt n)O(n),枚举各块O(n)O(\sqrt n)O(n)。所以一次操作的时间复杂度为O(n)O(\sqrt n)O(n),总时间复杂度O(nn)O(n\sqrt n)O(nn)。虽然相对于线段树的O(nlogn)O(nlogn)O(nlogn)分块还是略逊一筹,但是分块方便易懂,在数据规模较小的情况下,分块甚至能代替许多高级的数据结构。

例题

例1
给出一个长为nnn的数列,以及nnn次操作,操作涉及区间加法,单点查值。

板题,比较简单

code

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,v,op,l,r,c,a[1000005],k[100005];
void pt(){
    if(l%v!=1){
        int re=(l-1)/v*v+v;
        while(l<=r&&l<=re){
            a[l]+=c;++l;
        }
    }
    while(l/v<=r/v&&r-l+1>=v){
        k[(l-1)/v+1]+=c;l+=v;
    }
    while(l<=r){
        a[l]+=c;++l;
    }
}
int main()
{
    scanf("%d",&n);
    v=sqrt(n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++){
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op==0) pt();
        else printf("%d\n",a[r]+k[(r-1)/v+1]);
    }
}

例2
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。

同样地,将数列分为⌈n⌉\lceil\sqrt n\rceiln块,用数组aaa来按原序存储数列,用数组ttt来按各块排序后的顺序存储数组。
对于区间加法,将两边部分在aaa数组中一个一个处理,然后将两边部分所在的块放在ttt数组中重新排序。中间各块打上懒标记即可。
对于区间查询,两边部分枚举一下,中间各块每块在ttt数组中二分查找即可
总时间复杂度O(nlognn)O(nlogn\sqrt n)O(nlognn),具体方法看代码

code

#include<bits/stdc++.h>
using namespace std;
int n,v,op,l,r,ans;
long long c,a[200005],t[200005],k[10005];
int main()
{
    scanf("%d",&n);
    v=sqrt(n);
    for(int i=n+1;i<=n+v;i++) a[i]=t[i]=1e15;
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        t[i]=a[i];
    }
    for(int i=1;i<=n;i++){
        if(i%v==1){
            sort(t+i,t+i+v);
        }
    }
    for(int i=1;i<=n;i++){
        scanf("%d%d%d%lld",&op,&l,&r,&c);
        if(op==0){
            if(l%v!=1){
                int re=(l-1)/v*v+v;
                while(l<=r&&l<=re){
                    a[l]+=c;++l;
                }
                for(int i=re-v+1;i<=re;i++) t[i]=a[i];
                sort(t+re-v+1,t+re+1);
            }
            while(l/v<=r/v&&r-l+1>=v){
                k[(l-1)/v+1]+=c;l+=v;
            }
            if(l<=r){
                int re=(l-1)/v*v;
                while(l<=r){
                    a[l]+=c;++l;
                }
                for(int i=re+1;i<=re+v;i++) t[i]=a[i];
                sort(t+re+1,t+re+v+1);
            }
        }
        else{
            ans=0;
            if(l%v!=1){
                int re=(l-1)/v*v+v;
                while(l<=r&&l<=re){
                    if(a[l]<c*c-k[(l-1)/v+1]) ++ans;
                    ++l;
                }
            }
            while(l/v<=r/v&&r-l+1>=v){
                int dl=(l-1)/v*v+1,dr=(l-1)/v*v+v,mid,ot=(l-1)/v*v,re=(l-1)/v+1;
                while(dl<=dr){
                    mid=(dl+dr)/2;
                    if(t[mid]<c*c-k[re]){
                        ot=mid;dl=mid+1;
                    }
                    else dr=mid-1;
                }
                ans+=ot-(l-1)/v*v;l+=v;
            }
            if(l<=r){
                int re=(l-1)/v+1;
                while(l<=r){
                    if(a[l]<c*c-k[re]) ++ans;
                    ++l;
                }
            }
            printf("%d\n",ans);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值