0x44 分块

Posted a-sc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了0x44 分块相关的知识,希望对你有一定的参考价值。

0x44 分块

1. 介绍

? 分块的基本思想就是通过适当的划分,预处理一部分信息并保存下来,用空间换取时间。总之就是一种“优雅” 的暴力,遵循“大段维护,局部朴素”的思想。

2. 总结

  1. 划分区块

    t 为区块个数,len为区块长度,一般为n/t,有时候根据复杂度调整。

    int t=sqrt(n);
    for(int i=1;i<=t;++i){
     L[i]=(i-1)*sqrt(n)+1;
     R[i]=i*sqrt(n);
    }
    if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
  2. pos[]标记

    有时侯和预处理写在一起

    for(int i=1;i<=t;++i){
        for(int j=L[i];j<=R[i];++j){
            pos[j]=i;
        }
    }
  3. 查询

    int query(int l,int r){
     int p=pos[l],q=pos[r];
     if(p==q){
            //l,r在一个块内,一般局部朴素
     }
     else{
            //l,r不在一个块内, 那就对p+1~q-1块维护,l~R[p],L[q]~r段朴素处理
     }
    }
  4. 修改

    有的题有修改操作,就和处理查询一样。

3. 习题

  1. 一个简单的整数问题2

    技术图片

    • 这道题可以作为一道模板题。理解分块的过程。
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MA=1e5+5;
    
    int n,m,x,y,t;
    ll add[MA],sum[MA];
    int a[MA],pos[MA],L[MA],R[MA];
    ll d;
    
    void Change(int l,int r,ll val){
     int p=pos[l],q=pos[r];
     if(p==q){           //l,r在一个块里面,朴素修改
         for(int i=l;i<=r;++i) a[i]+=val;
         sum[p]+=val*(r-l+1);
     }
     else{
         for(int i=p+1;i<=q-1;++i) add[i]+=val;  //给p+1到q-1块打标记
         for(int i=l;i<=R[p];++i) a[i]+=val;     //处理最左端不满一块的区域
         sum[p]+=val*(R[p]-l+1);
         for(int i=L[q];i<=r;++i) a[i]+=val;     //处理最右端不满一块的区域
         sum[q]+=val*(r-L[q]+1);
     }
    }
    
    ll query(int l,int r){
     int p=pos[l],q=pos[r];
     ll ans=0;
     if(p==q){
         for(int i=l;i<=r;++i) ans+=a[i];
         ans+=add[p]*(r-l+1);
     }
     else{
         for(int i=p+1;i<=q-1;++i) ans+=sum[i]+add[i]*(R[i]-L[i]+1);
         for(int i=l;i<=R[p];++i) ans+=a[i];
         ans+=add[p]*(R[p]-l+1);
         for(int i=L[q];i<=r;++i) ans+=a[i];
         ans+=add[q]*(r-L[q]+1);
     }
     return ans;
    }
    
    int main()
    {
     //输入
     memset(add,0,sizeof(add));
     scanf("%d%d",&n,&m);
     for(int i=1;i<=n;++i) scanf("%d",&a[i]);
     //分块,确定各个分块左右端点
     t=sqrt(n);
     for(int i=1;i<=t;++i){
         L[i]=(i-1)*sqrt(n)+1;
         R[i]=i*sqrt(n);
     }
     if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
    
     for(int i=1;i<=t;++i){          //预处理sum[], pos[]
         for(int j=L[i];j<=R[i];++j){
             sum[i]+=a[j];
             pos[j]=i;
         }
     }
     //指令处理
     while(m--){
         char op[5];
         scanf("%s",&op);
         if(op[0]=='C'){
             scanf("%d %d %lld",&x,&y,&d);
             Change(x,y,d);
         }
         else if(op[0]=='Q'){
             scanf("%d%d",&x,&y);
             printf("%lld
    ",query(x,y));
         }
     }
     return 0;
    }
    
  2. 蒲公英

    技术图片

    • 在线求区间众数
    • 用vector保存每个数出现的位置,在预处理时,对处理区间每个数,在对应存位置向量中二分查找区间边界l和r的下标位置,作差就是这个值在区间中的出现次数。然后用这个次数更新答案。
    • 预处理任意2个块的答案,为什么要预处理任意2区块之间的答案呢?,这是由于区间众数不具有“区间可加性”,为了减少复杂度,所以要先预处理
    • “大段维护,小段朴素”
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MA=4e4+5;
    
    int a[MA],b[MA],c[MA],pos[MA],L[MA],R[MA],f[805][805]; //c:用来计数,pos:标记所属块,L/R:块边界,f:任意区间答案
    vector<int> e[MA];
    
    int find(int x,int l,int r){ //确定数字x在l,r区间中的个数
        return upper_bound(e[x].begin(),e[x].end(),r) - lower_bound(e[x].begin(),e[x].end(),l);
    }
    
    void work(int x,int l,int r,int &cnt,int &ans){
        int w=find(x,l,r);      //w记录x在l,r中的个数
        if(w>cnt||(w==cnt&&x<ans)){         //更新答案
            cnt=w;
            ans=x;
        }
    }
    
    int query(int l,int r){
        int p=pos[l],q=pos[r];
        int ans=0,cnt=0;
        if(p==q){               ////小段:朴素查询l,r之间的每个值
            for(int i=l;i<=r;++i) work(a[i],l,r,cnt,ans);
            return b[ans];
        }
        int x=0,y=0;
        if(p+1<=q-1){
            x=p+1;
            y=q-1;
        }
        for(int i=l;i<=R[p];++i) work(a[i],l,r,cnt,ans);
        for(int i=L[q];i<=r;++i) work(a[i],l,r,cnt,ans);
        if(f[x][y]) work(f[x][y],l,r,cnt,ans);
        return b[ans];
    }
    
    int main()
    {
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;++i) scanf("%d",&a[i]);
        memcpy(b,a,sizeof(b));
        sort(b+1,b+n+1);
        int tot=unique(b+1,b+n+1)-(b+1);
        for(int i=1;i<=n;++i){
            a[i]=lower_bound(b+1,b+tot+1,a[i]) - b;
            e[a[i]].push_back(i);
        }
    
        int t=sqrt(log(n)/log(2)*n);
        int len=t?n/t:n;
        for(int i=1;i<=t;++i){
            L[i]=(i-1)*len+1;
            R[i]=i*len;
        }
        if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
        for(int i=1;i<=t;++i)
            for(int j=L[i];j<=R[i];++j)
                pos[j]=i;
    
        for(int i=1;i<=t;++i){
            memset(c,0,sizeof(c));
            int ans=0,cnt=0;
            for(int j=L[i];j<=n;++j){
                if(++c[a[j]]>cnt||(c[a[j]]==cnt&&a[j]<ans)){
                    cnt=c[a[j]];
                    ans=a[j];
                }
                f[i][pos[j]]=ans;
            }
        }
        int x=0;
        while(m--){
            int l,r;
            scanf("%d%d",&l,&r);
            l=(l+x-1)%n+1;
            r=(r+x-1)%n+1;
            if(l>r) swap(l,r);
            x=query(l,r);
            printf("%d
    ",x);
        }
        return 0;
    }
    
  3. 磁力距

    技术图片

    • 一开始没有头绪,就想暴力算法,(O(n^{2})) 复杂度,既然分块是优雅的暴力,那应该可以利用分块降低复杂度。
    • 将所有点按到 ((x0,y0)?) 的距离的平方大小排序,这一就将所有点由二维压缩到一维坐标上。然后就可以将它们分块了。
    • 分成 (sqrt n) 块 ,每一块内部再按照质量大小排序,这一步写在预处理里面。
      我们用一个队列存到手的磁石(感觉这类题,一个点可以得到其他一些点的题用队列很方便模拟)。遍历所有分块,如果这个分块最大的距离dis[i]大于当前处理的点 now 的磁力半径。(因为按距离从小到大排序) 那么之后的分块就都不用考虑,只需要把当前分块处理一下,就跳出。否则,将整个分块都处理一下。
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MA=2e6+5;
    
    struct node
    {
        ll m,d,r,p;
    }a[MA];
    int pos[MA],L[MA],R[MA],vis[MA],l,r,n;
    ll dis[MA],x,y;
    queue<int> q;
    int x0,y_0;
    
    bool cmp_m(const node &a,const node &b) {return a.m<b.m;}
    bool cmp_d(const node &a,const node &b) {return a.d<b.d;}
    
    int main()
    {
        scanf("%d%d%lld%lld%d",&x0,&y_0,&a[0].p,&a[0].r,&n);
        a[0].r*=a[0].r;
        for(int i=1;i<=n;++i){
            scanf("%lld%lld%lld%lld%lld",&x,&y,&a[i].m,&a[i].p,&a[i].r);
            a[i].r*=a[i].r;
            a[i].d=(x0-x)*(x0-x)+(y_0-y)*(y_0-y);
        }
        sort(a+1,a+n+1,cmp_d);
    
        int t=sqrt(n);
        for(int i=1;i<=t;++i){
            L[i]=(i-1)*sqrt(n)+1;
            R[i]=i*sqrt(n);
            dis[i]=a[R[i]].d;
            sort(a+L[i],a+R[i]+1,cmp_m);
        }
        if(R[t]<n){
            t++,L[t]=R[t-1]+1,R[t]=n;
            dis[t]=a[R[t]].d;
            sort(a+L[t],a+R[t]+1,cmp_m);
        }
    
        q.push(0);
        vis[0]=1;
        int ans=0;
        while(q.size()){
            int now=q.front();
            q.pop();
            for(int i=1;i<=t;++i){
                if(dis[i]>a[now].r){
                    for(int j=L[i];j<=R[i];++j){
                        if(!vis[j] && a[j].m<=a[now].p && a[j].d<=a[now].r){
                            q.push(j);
                            //cout<<j<<endl;
                            vis[j]=1;
                            ans++;
                        }
                    }
                    break;
                }
                while(L[i]<=R[i]&&a[L[i]].m<=a[now].p){
                    if(!vis[L[i]]){
                        q.push(L[i]);
                        //cout<<L[i]<<endl;
                        ans++;
                    }
                    ++L[i];
                }
            }
        }
        printf("%d
    ",ans);
        return 0;
    }
    

以上是关于0x44 分块的主要内容,如果未能解决你的问题,请参考以下文章

分块代码

分块简单入门

.NET 中的分块编码实现(或至少是伪代码)

如何理解使用 izip_longest 对列表进行分块的代码?

C++ 实现分块查找(顺序存储结构)(完整代码)

C++ 实现分块查找(链式存储结构)(完整代码)