学习笔记:分块
分块
引入
众所周知,我们熟悉的算法时间复杂度有常数级,对数级、线性级、次方级、指数级等等,其中为应对题目规模对时间复杂度的要求,我们一般要将算法的时间复杂度优化到对数级,但是实际上我们还有一种优化方法——根号算法,它的时间复杂度为根号级,同样可以应对大部分的题目规模,并且具有相当大的可拓展性。和对数算法基本对应分治类似,根号算法也对应着一种操作,就是本篇笔记要介绍的分块。
简介
分块,顾名思义就是分成几块(是这样的嘛),又被称之为“优雅的暴力”。分块的主要做法就是将一大段序列分成几块,以块为单位维护区间的值,为了时间复杂度的均摊,块的大小通常设置为 。在询问时,分块则采取“大段维护,局部朴素”的思想,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
对于区间更新,若区间不跨越块,直接暴力处理,时间复杂度 ,若跨越块,则不能凑成一个整块的暴力处理,整块的直接更新,时间复杂度 。区间查询,与区间更新一样,为 。
分块实质上就是三层的树,每个非叶子结点的节点有 个子节点。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,并不要求所维护信息满足结合律,也不需要一层层地传递标记,可以维护很多树状数组和线段树无法维护的信息。
当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。不过在大多数问题上,分块仍然是解决这些问题的一个不错选择。
实现
首先搬运一道板子题。
P3372 【模板】线段树 1
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 。
- 求出某区间每一个数的和。
思路
这道题也可以用线段树和树状数组做,但现在我们用分块做。
首先确定分块的长度,再预处理出每一个元素所属的块的编号和每一大块的答案。
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;
}
至于分块时每一大块具体的长度,由均值不等式可知,分块的最佳长度是 。
#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;
}
时间复杂度
首先看查询操作:
- 若 和 在同一个块内,直接暴力求和即可,因为块长为 ,因此最坏复杂度为 。
- 若 和 不在同一个块内,则答案由三部分组成:以 开头的不完整块,中间几个完整块,以 结尾的不完整块。对于不完整的块,仍然采用上面暴力计算的方法,对于完整块,则直接利用已经求出的 求和即可。这种情况下,最坏复杂度为 。
接下来是修改操作:
- 若 和 在同一个块内,直接暴力修改即可,因为块长为 ,因此最坏复杂度为 。
- 若 和 不在同一个块内,则需要修改三部分:以 开头的不完整块,中间几个完整块,以 结尾的不完整块。对于不完整的块,仍然是暴力修改每个元素的值(别忘了更新区间和 ),对于完整块,则直接修改 即可。这种情况下,最坏复杂度和仍然为 。
要使分块复杂度最优,则 和 都应尽可能地取到最小值。
对于 ,利用均值不等式可知,显然有:
当且仅当 ,即 时取等号。此时单次操作的时间复杂度最优,为 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App