分块
分块算法的思想是通过适当的划分,通过预处理,用空间换取时间,达到时空平衡
基本操作是,将一段序列,分成一定数量的块,每一块有一个长度,表示一段区间
一般来讲,块的大小常设为$\sqrt{n}$,但实际上块的大小可以任意自定,不过肯定是要让复杂度尽可能的低
分块的效率虽然低于树状数组和线段树,但代码实现相对简单,也更好理解
但是俗话说的好:越简单的东西,就意味着它越牢固,可拓展性越强
用法
分块支持区间修改和区间查询,与其说是数据结构,它其实更像是一种思想,相当于分治的一个分支。
分块的基本操作:
划分块,预处理,操作或查询
操作或查询通常为4步:
1. 判断要操作或是查询的区间是否只在一个块内
2. 若只在一个块内,暴力操作或查询
3. 若不只在一个块内,将除了最左边和最右边这两个块外,其余中间的整块进行整体的操作,即直接对块打上修改标记
4. 单独暴力处理左边剩余的碎块和右边剩余的碎块
即“左碎+整块+右碎”,“单点+区间+单点”解决区间的修改与查询问题
常用数组意义及用法:
//就按照自己的习惯来了 struct dian{ long long zhi;//该点当前的数值 int k;//该点所在的块的编号 }; dian a[NUM]; long long sum[NUM];//第i个块中所有点的总和 long long tag[NUM];//第i个块的懒惰标记 int n; int w;//每个块里点的数量 void add( int l,int r,int p ); void query( int l,int r ); int main(){ cin >> n; w = sqrt(n); for( int i = 1;i <= n;i++ ){ cin >> a[i].zhi; a[i].k = (i-1) / w + 1;//第i个点所在的块编号 sum[a[i].k] += a[i].zhi; } int l,r,p; cin >> l >> r >> p; add( l,r,p );//区间修改 query( l,r );//区间查询 } void add( int l,int r,int p ){ //区间加 for( int i = l;i <= min(r,a[l].k * w);i++ ){ //左边的碎块 //l是增值的起点, a[l].k * w是起点所在块的最后一个 //取min是预防r与l在一个块里 a[i].zhi += p; //点本身加 sum[a[i].k] += p;//点所在块加 } if( a[l].k != a[r].k ){ //如果起点终点不在同一个块,那就说明有右碎块 for( int i = (a[r].k-1)*w+1;i <= r;i++ ){ //从右碎块的前一个块的结尾的后一个点开始,即碎块的第一个 a[i].zhi += p; //同左碎块 sum[a[i].k] += p; } } for( int i = a[l].k+1;i <= a[r].k-1;i++ ) //整块是除去起点所在块与终点所在块的中间所有块 tag[i] += p; //整块直接加在懒惰标记上 } void query( int l,int r ){ //查询 //其实大体结构跟add函数差不多 long long ans = 0; for( int i = l;i <= min(r,a[l].k * w);i++ ) //左边的碎块 ans += a[i].zhi + tag[a[i].k]; //一个点的真实值为本身值加所在块的懒惰标记值 if( a[l].k != a[r].k ){ for( int i = (a[r].k-1)*w+1;i <= r;i++ ) //右边的碎块 ans += a[i].zhi + tag[a[i].k]; } for( int i = a[l].k+1;i <= a[r].k-1;i++ ) //整块 ans += sum[i] + tag[i] * w;//懒惰标记是每个人都有的,所以要乘以成员数量 } 分块的用法板子
以题目作为一个例子,具体感受分块的用法及精髓:洛谷P2357守墓人 题解