学习笔记:分块

分块

引入

众所周知,我们熟悉的算法时间复杂度有常数级,对数级、线性级、次方级、指数级等等,其中为应对题目规模对时间复杂度的要求,我们一般要将算法的时间复杂度优化到对数级,但是实际上我们还有一种优化方法——根号算法,它的时间复杂度为根号级,同样可以应对大部分的题目规模,并且具有相当大的可拓展性。和对数算法基本对应分治类似,根号算法也对应着一种操作,就是本篇笔记要介绍的分块。

简介

分块,顾名思义就是分成几块(是这样的嘛),又被称之为“优雅的暴力”。分块的主要做法就是将一大段序列分成几块,以块为单位维护区间的值,为了时间复杂度的均摊,块的大小通常设置为 O(n)。在询问时,分块则采取“大段维护,局部朴素”的思想,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。

对于区间更新,若区间不跨越块,直接暴力处理,时间复杂度 O(n),若跨越块,则不能凑成一个整块的暴力处理,整块的直接更新,时间复杂度 O(n)。区间查询,与区间更新一样,为 O(n)

分块实质上就是三层的树,每个非叶子结点的节点有 n 个子节点。

分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,并不要求所维护信息满足结合律,也不需要一层层地传递标记,可以维护很多树状数组和线段树无法维护的信息。

当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。不过在大多数问题上,分块仍然是解决这些问题的一个不错选择。

实现

首先搬运一道板子题。

P3372 【模板】线段树 1

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 k
  2. 求出某区间每一个数的和。

思路

这道题也可以用线段树和树状数组做,但现在我们用分块做。

首先确定分块的长度,再预处理出每一个元素所属的块的编号和每一大块的答案。

    for(int i = 1 ; i <= n ; i ++)a[i] = read();
    for(int i = 1 ; i <= n ; i ++)id[i] = (i - 1) / len + 1;
    for(int i = 1 ; i <= n ; i ++)s[id[i]] += a[i];

之后对于每一个询问,我们都采取“大段维护,局部朴素”的思想,对于每一次大块的修改操作,我们都使用一个懒标记(跟线段树那个差不多)来记录,在查询时再加上。

void add(int l, int r, int k){
    int left = id[l], right = id[r];
    if(left == right){
        for(int i = l ; i <= r ; i ++)a[i] += k;
        for(int i = l ; i <= r ; i ++)s[id[i]] += k;
    }else{
        for(int i = l ; id[i] == left ; i ++)a[i] += k;
        for(int i = l ; id[i] == left ; i ++)s[id[i]] += k;
        for(int i = left + 1 ; i < right ; i ++)mark[i] += k;
        for(int i = left + 1 ; i < right ; i ++)s[i] += len * k;
        for(int i = r ; id[i] == right ; i --)a[i] += k;
        for(int i = r ; id[i] == right ; i --)s[id[i]] += k;
    }
}
int query(int l, int r){
    int left = id[l], right = id[r], res = 0;
    if(left == right){
        for(int i = l ; i <= r ; i ++)res = res + a[i] + mark[id[i]];
    }else{
        for(int i = l ; id[i] == left ; i ++)res = res + a[i] + mark[id[i]];
        for(int i = left + 1 ; i < right ; i ++)res += s[i];
        for(int i = r ; id[i] == right ; i --)res = res + a[i] + mark[id[i]];
    }
    return res;
}

至于分块时每一大块具体的长度,由均值不等式可知,分块的最佳长度是 n

#include <iostream>
#include <cmath>
#define int long long
#define MAXN 100005
#define MAXM 100005
using namespace std;
int n, m, len, op, x, y, k;
int a[MAXN], s[MAXN], id[MAXN], mark[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int l, int r, int k){
    int left = id[l], right = id[r];
    if(left == right){
        for(int i = l ; i <= r ; i ++)a[i] += k;
        for(int i = l ; i <= r ; i ++)s[id[i]] += k;
    }else{
        for(int i = l ; id[i] == left ; i ++)a[i] += k;
        for(int i = l ; id[i] == left ; i ++)s[id[i]] += k;
        for(int i = left + 1 ; i < right ; i ++)mark[i] += k;
        for(int i = left + 1 ; i < right ; i ++)s[i] += len * k;
        for(int i = r ; id[i] == right ; i --)a[i] += k;
        for(int i = r ; id[i] == right ; i --)s[id[i]] += k;
    }
}
int query(int l, int r){
    int left = id[l], right = id[r], res = 0;
    if(left == right){
        for(int i = l ; i <= r ; i ++)res = res + a[i] + mark[id[i]];
    }else{
        for(int i = l ; id[i] == left ; i ++)res = res + a[i] + mark[id[i]];
        for(int i = left + 1 ; i < right ; i ++)res += s[i];
        for(int i = r ; id[i] == right ; i --)res = res + a[i] + mark[id[i]];
    }
    return res;
}
signed main(){
    n = read();m = read();len = sqrt(n);
    for(int i = 1 ; i <= n ; i ++)a[i] = read();
    for(int i = 1 ; i <= n ; i ++)id[i] = (i - 1) / len + 1;
    for(int i = 1 ; i <= n ; i ++)s[id[i]] += a[i];
    for(int i = 1 ; i <= m ; i ++){
        op = read();
        switch(op){
            case 1:
                x = read();y = read();k = read();
                add(x, y, k);break;
            case 2:
                x = read();y = read();
                write(query(x, y));putchar('\n');
        }
    }
    return 0;
}

时间复杂度

首先看查询操作:

  • lr 在同一个块内,直接暴力求和即可,因为块长为 s,因此最坏复杂度为 O(s)
  • lr 不在同一个块内,则答案由三部分组成:以 l 开头的不完整块,中间几个完整块,以 r 结尾的不完整块。对于不完整的块,仍然采用上面暴力计算的方法,对于完整块,则直接利用已经求出的 bi 求和即可。这种情况下,最坏复杂度为 O(ns+s)

接下来是修改操作:

  • lr 在同一个块内,直接暴力修改即可,因为块长为 s,因此最坏复杂度为 O(s)
  • lr 不在同一个块内,则需要修改三部分:以 l 开头的不完整块,中间几个完整块,以 r 结尾的不完整块。对于不完整的块,仍然是暴力修改每个元素的值(别忘了更新区间和 bi),对于完整块,则直接修改 bi 即可。这种情况下,最坏复杂度和仍然为 O(ns+s)

要使分块复杂度最优,则 ns+ss 都应尽可能地取到最小值。

对于 ns+s,利用均值不等式可知,显然有:

ns+s2ns×s=2n

当且仅当 ns=s,即 s=n 时取等号。此时单次操作的时间复杂度最优,为 O(n)

posted @   tsqtsqtsq  阅读(9)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示