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

分块: 优雅的暴力算法

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

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

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

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

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

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

分块的具体操作

P3372 线段树1 为例.

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

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

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

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

  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\n", 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

posted @ 2020-07-08 16:58  ChPu437  阅读(196)  评论(1编辑  收藏  举报