[模板][数据结构] 分块
分块: 优雅的暴力算法
分块这么名字, 听起来十分的高大上 并没有 , 但是实际 板子 理解起来不是很难.
对于一个需要维护某种区间上的信息的序列, 我们可以将其拆分为几个子区间, 也就是块 . 这样, 对于每一个块, 我们可以分别维护块中的某一个值, 在查询区间中的该值时, 只需要将区间落到块上, 进行求解, 就可以大大缩小复杂度.
相较于线段树( \(O(\log_2n)\) ), 分块本身的复杂度是比较高(\(O(\sqrt n)\))的.
但是, 分块( 相对线段树 )有如下优点:
- 代码相对短小好写
- 易调试
- 常数小
在考试时, 分块也不失为一种得 骗 分的好办法.
分块的具体操作
以 P3372 线段树1 为例.
首先, 我们将整个范围分成若干一定大小的块.
- 关于块的大小问题, 一般将长度为 \(n\) 的序列分为大小为 \(\sqrt n\) 的若干块, 这样的时间复杂度较为稳定.
- 若 \(n\) 不是完全平方数, 便会生成出一个较小的块, 这个块叫做角块.
然后我们对于每个块像线段树的区间一样进行预处理, 处理出每个块的总和.
接下来进行操作, 对于每次操作( 无论是修改还是查询 )的区间 \(\left[l,r\right]\) , 都有如下的几种情况:
-
\(l\), \(r\) 在同一个块中:
此时我们无法利用分块的性质, 但因为范围不大( 最大\(\sqrt n\) ), 所以我们可以放心的直接操作.
-
\(l\), \(r\) 在相邻的两个块中:
法同 1.
-
\(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;
}