• [模板][数据结构] 分块


    分块: 优雅的暴力算法

    分块这么名字, 听起来十分的高大上 并没有 , 但是实际 板子 理解起来不是很难.

    对于一个需要维护某种区间上的信息的序列, 我们可以将其拆分为几个子区间, 也就是 . 这样, 对于每一个块, 我们可以分别维护块中的某一个值, 在查询区间中的该值时, 只需要将区间落到块上, 进行求解, 就可以大大缩小复杂度.

    相较于线段树( (O(log_2n)) ), 分块本身的复杂度是比较高((O(sqrt n)))的.

    但是, 分块( 相对线段树 )有如下优点:

    • 代码相对短小好写
    • 易调试
    • 常数小

    在考试时, 分块也不失为一种得 分的好办法.

    分块的具体操作

    P3372 线段树1 为例.

    首先, 我们将整个范围分成若干一定大小的块.

    • 关于块的大小问题, 一般将长度为 (n) 的序列分为大小为 (sqrt n) 的若干块, 这样的时间复杂度较为稳定.
    • (n) 不是完全平方数, 便会生成出一个较小的块, 这个块叫做角块.

    然后我们对于每个块像线段树的区间一样进行预处理, 处理出每个块的总和.

    接下来进行操作, 对于每次操作( 无论是修改还是查询 )的区间 (left[l,r ight]) , 都有如下的几种情况:

    1. (l), (r) 在同一个块中:

      此时我们无法利用分块的性质, 但因为范围不大( 最大(sqrt n) ), 所以我们可以放心的直接操作.

      1. (l), (r)相邻的两个块中:

        法同 1.

      2. (l), (r) 在不相邻的块中:

        这时, 对于两边的块( (l), (r) 两端点所在的块 ), 我们仍然直接处理, 但是中间的完整的块, 我们就可以直接利用之前维护的值来更新了.

    • 说明一下区间修改: 我们对每个区间维护一个标记, 需要修改时直接修改标记, 这样最后输出答案时处理一下标记即可.

    总复杂度: (O(sqrt n)).

    附:实际上, 分块是一个 "原序列 -- 块 -- 值" 三层的树形结构.

    例题代码

    hzwer神仙的分块九练为例.

    所有 (9) 道题的传送门均为LibreOJ

    数列分块入门 1

    一个基本的板子, 没有什么特别的地方.

    # include <iostream>
    # include <cstdio>
    # include <cmath>
    # define MAXN 50005
    
    using namespace std;
    
    int a[MAXN];
    int blk[MAXN], sizB, tagA[250]; // 每个点所属的块, 块的大小, 每个块的加标记
    
    void Add(int l, int r, int val){
         if(blk[r] - blk[l] <= 1){
             for(int i = l; i <= r; i++)
                 a[i] += val;
             return;
         } // 两点在一块或相邻块
    
        for(int i = l; i <= blk[l]*sizB; i++)
            a[i] += val;
        for(int i = (blk[r]-1)*sizB+1; i <= r; i++)
            a[i] += val; // 处理角块
    
        for(int i = blk[l]+1; i <= blk[r]-1; i++)
            tagA[i] += val;
    
    }
    
    
    int main(){
        int n, opt, l, r, c;
        scanf("%d", &n);
        sizB = sqrt(n);
    
        for(int i = 1; i <= n; i++){
            scanf("%d", &a[i]);
            blk[i] = (i-1) / sizB + 1;
        }
    
        for(int i = 1; i <= n; i++){
            scanf("%d%d%d%d", &opt, &l, &r, &c);
            if(opt){
               printf("%d
    ", tagA[blk[r]] + a[r]);
            }
            else{
                Add(l, r, c);
            }
        }    
    
        return 0;
    }
    

    数列分块入门 2

    考虑到需要查询某区间内小于 (c^2) 的个数, 我们需要对块进行排序, 这样可以通过二分查找快速解决问题.

    # include <iostream>
    # include <cstdio>
    # include <cmath>
    # include <algorithm>
    # include <vector>
    # define MAXN 50005
    
    using namespace std;
    
    int a[MAXN], blk[MAXN], tagA[250], sizB, n; 
    vector<int>v[250]; // v 用于维护每个块中的顺序
    
    int Query(int l, int r, int val){
        int ans = 0;
        // 对于每一个部分分别处理
        for(int i = l; i <= min(blk[l]*sizB, r); i++)
            if(a[i] + tagA[blk[i]] < val)
                ans++;
        
        if(blk[l] != blk[r])
            for(int i = (blk[r]-1)*sizB+1; i <= r; i++)
                if(a[i] + tagA[blk[i]] < val)
                    ans++;
        
        for(int i = blk[l]+1; i <= blk[r]-1; i++){
            int tmp = val - tagA[i];
            ans += lower_bound(v[i].begin(), v[i].end(), tmp) - v[i].begin();
        }
        return ans;
    }
    
    void Reset(int x){
        v[x].clear();
        for(int i = (x-1)*sizB+1; i <= min(x*sizB, n); i++)
            v[x].push_back(a[i]);
        sort(v[x].begin(), v[x].end());
    } // 清空块重新排序
    
    void Add(int l, int r, int val){
        if(blk[l] == blk[r]){
            for(int i = l; i <= r; i++) 
                a[i] += val;
            Reset(blk[l]);    
            return;
        }
        for(int i = l; i <= blk[l]*sizB; i++)   
            a[i] += val;
        for(int i = (blk[r]-1)*sizB+1; i <= r; i++)
            a[i] += val;
        for(int i = blk[l]+1; i <= blk[r]-1; i++)
            tagA[i] += val;
        Reset(blk[l]); Reset(blk[r]);
        // 完整的块每个数都被加上了一个定值, 单调性不改变
    }
    
    signed main(){
        int opt, l, r, c;
    
        cin>>n;
        sizB = sqrt(n);
        for(int i = 1; i <= n; i++){
            cin>>a[i]; 
            blk[i] = (i-1) / sizB + 1;
            v[blk[i]].push_back(a[i]);
        }
        for(int i = 1; i <= blk[n]; i++)
            sort(v[i].begin(), v[i].end());
        for(int i = 1; i <= n; i++){
            cin>>opt>>l>>r>>c;
            if(opt){
                cout<<Query(l, r, pow(c, 2))<<endl;
            }
            else{
                Add(l, r, c);
            }
        }
        return 0;
    }
    

    数列分块入门 3

    数列分块入门 4

    数列分块入门 5

    数列分块入门 6

    数列分块入门 7

    数列分块入门 8

    数列分块入门 9

  • 相关阅读:
    go-zero尝试运行输出hello-world
    grpc客户端 服务端测试
    protobuf序列化
    protobuff3语法详情
    【转】普通程序员如何转向AI方向
    深度学习微软 azure-云服务器组 centos特殊内核版本 gpu NVIDIA 驱动及CUDA 11.0安装
    分享一个主要用于nas场景的集成了迅雷,百度网盘等软件的docker ubuntu vnc镜像-适用于x86环境
    以spark sql 维护spark streaming offset
    打通es及lucene应用,lucene应用es Query,应用完整的es query
    打通es及lucene应用,lucene应用es Query,结合非queryString查询(二)
  • 原文地址:https://www.cnblogs.com/Foggy-Forest/p/13267943.html
Copyright © 2020-2023  润新知