引入 foreword
数列分块就是把数列中【每m个元素打包起来】,达到优化算法的目的。
把每m个元素分为一块,共有n/m块,区间修改涉及O(n/m)个整块,以及两侧两个不完整的块。
每次操作对每个整块直接标记,而由于不完整的块的元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。每次操作的复杂度是O(n/m)+O(m)。
根据均值不等式,当m取√n时总复杂度最低,所以默认分块大小为√n,复杂度为O(√n)。
一. 分块的常用数组
int n,m; //总个数n,每块的大小m
int a[500019],tag[500019]; //原数组 和 标记数组(对于每一块)
int pos[500019]; //pos[i]=(i-1)/m+1,即记录i属于哪一块
还有一些在特定情况下使用的数组:
vector<int> v[519]; //记录每块的元素,并分别排序
//在每块中维护单调性,用lowerbound函数维护块的满足条件的值的个数
set<int> s[519]; //set记录每块的元素(已分别排序去重)
bool okk[500019]; //标记整个块内的元素是否全部满足某个条件
int b[N]; //b[i]用于数组离散化 或者 重新分块
int f[4021][4021]; //预处理出从第i块到第j块的总信息(比如区间众数)
int tag_add[500019],tag_mul[500019]; //多种标记...
二. 分块的常用函数
(1) void add(int l,int r,int x):用于区间的修改。在该函数中更新标记数组tag。
【add函数中一般包含三个步骤】a.处理l的边界块,暴力更新整块;
b.处理r的边界块,暴力更新整块;c.在中间的所有整块,打上标记。
(2) int query(int l,int r,int x):用于区间的查询。在该函数中整合所有修改值。
【query函数中一般包含三个步骤】a.处理l的边界块,暴力求值;
b.处理r的边界块,暴力求值;c.在中间的所有整块,整合所有标记,整体计算。
(3) void rebuild():重新分块。一般用于会添加元素的分块过程中。
【rebuild函数中一般包含三个步骤】a.记录a数组中的原状态到中转数组b中;
b.得到新的块大小,修改各个数组;c.将b数组的值完全赋给a数组。
(4) 数组的离散化+分块。一般用于数较大的时候,节约内存。
1.把a数组复制到b数组中; 2.b数组排序+去重;
3.将a数组中的数用【在b数组中的排名】表示出来;
4.完成离散化后,输出原值时,调用 b [ a [ i ] ] 。
sort(b+1,b+n+1); int n0=unique(b+1,b+n+1)-(b+1); //排序去重
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+n0+1,a[i])-b;
//↑↑寻找数值a[i]在离散化数组b中对应位置的下标(类似编号,排名)
(5) 其他函数。一般用于块间初始化问题,或者特殊的块间修改情况。
三. 分块练习题
【练习1】区间修改,单点查询
- 每块标记tag,剩下的l、r两个边界块直接修改。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习1】区间修改,单点查询。*/
/*【分析】数列分块就是把数列中【每m个元素打包起来】,达到优化算法的目的。
如果把每m个元素分为一块,共有n/m块,区间加会涉及O(n/m)个整块,以及两侧两个不完整的块。
每次操作对每个整块直接标记,而由于不完整的块的元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。每次操作的复杂度是O(n/m)+O(m)。
根据均值不等式,当m取√n时总复杂度最低,所以默认分块大小为√n,复杂度为O(√n)。*/
int n,m,pos[500019]; //pos[i]=(i-1)/m+1,即i属于哪一块
int a[500019],tag[500019];
//↓↓每块标记tag,剩下的l、r两个边界块直接修改
void adds(int l,int r,int x){ //l,r同段 或者 先处理l的边界段
for(int i=l;i<=min(r,pos[l]*m);i++) a[i]+=x;
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(int i=(pos[r]-1)*m+1;i<=r;i++) a[i]+=x;
for(int i=pos[l]+1;i<=pos[r]-1;i++) tag[i]+=x;
}
int main(/*hs_love_wjy*/){
scanf("%d",&n); m=sqrt(n);
for(int i=1;i<=n;i++) pos[i]=(i-1)/m+1;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1,op,l,r,x;i<=n;i++){
scanf("%d%d%d%d",&op,&l,&r,&x);
if(op==0) adds(l,r,x);
if(op==1) printf("%d\n",tag[pos[r]]+a[r]);
} //↑↑op=1时,l、x输入但忽略
}
【练习2】区间修改,查询比x小的个数
- vector数组记录每块的元素,排序维护块内的递增性。
- 完整的块可以直接用lower_bound返回第一个大于等于x的位置。
- 不完整的块直接暴力修改,但需要在每块内每次重新排序。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习2】区间修改,查询比x小的个数。*/
/*【分析】记录每块的元素,排序维护块内的递增性。
完整的块可以直接用lower_bound返回第一个大于等于x的位置。
不完整的块直接暴力修改,但需要在每块内每次重新排序。*/
int n,m,pos[500019]; //pos[i]=(i-1)/m+1,即i属于哪一块
int a[500019],tag[500019];
//↑↑对整块使用tag标记,在询问的时候再计算tag的影响
vector<int> v[519]; //记录每块的元素,并分别排序
void changes(int num){
v[num].clear(); //↓↓如果是最后一块,可能不完整
for(int i=(num-1)*m+1;i<=min(num*m,n);i++)
v[num].push_back(a[i]);
sort(v[num].begin(),v[num].end());
}
//↓↓每块标记tag,剩下的l、r两个边界块直接修改
void adds(int l,int r,int x){ //l,r同段 或者 先处理l的边界段
for(int i=l;i<=min(r,pos[l]*m);i++) a[i]+=x;
changes(pos[l]); //将