分块 学习笔记
前言:内容参考自https://www.luogu.com.cn/blog/expect/solution-p2801。感谢。
---------------------
分块,是一种优雅的暴力,它通过对数列分段,完成对数列一些区间操作和区间查询的操作,是一种根号算法。本文属于分块入门笔记,旨在零基础的同学学会分块。
1 建块
在建块伊始,我们需要完成一下几个任务:
1.确定块的大小
2.确定块的数量
3.标记每个块的左右边界
4.标记每个元素所属的块
5.对块内的元素进行预处理
1.1.块的大小:为了保证时间复杂度最优,一般我们让块的大小为$\sqrt(n)$。写起来就一句话:
int size=sqrt(n);
1.2.块的数量:显然为$n/size$。如果$n$不是完全平方数,则让数量+1。
int tot=n/size; if (n%tot) tot++;
1.3.块的端点:对于每个块的左右端点,显然$L[1]=1,R[1]=block,L[2]=R[1]+1,R[2]=block*2,\cdots$,所以我们可以得到:
for (int i=1;i<=tot;i++) L[i]=(i-1)*size+1,R[i]=i*size;
1.4.所属的块:同样,我们可以得到每个元素所属的块:
for (int i=1;i<=n;i++) belong[i]=(i-1)/size+1;
1.5.初始化:根据题目的要求来进行操作。一般都有预处理区间和,或者对元素进行排序。
2.修改
分块题常见的操作就是区间修改。对于修改区间$(x,y)$,我们一般这样处理:
$(x,y)$的左边和右边可能不是属于一个整块的。对于这些边边角角,我们暴力处理即可。
对于中间的整块,我们考虑线段树区间修改的思想:$lazy tag$。我们只需让$add[i]+=k$即可,这就是分块算法较为高效的地方。
注意特判$belong[x]==belong[y]$的情况,此情况暴力处理即可。
代码如下(以洛谷P2801为例):
inline void update() { if (belong[x]==belong[y])//特判 { for (int i=x;i<=y;i++) a[i]+=k; for (int i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); return; } for (int i=x;i<=R[belong[x]];i++) a[i]+=k;//暴力处理 for (int i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); for (int i=L[belong[y]];i<=y;i++) a[i]+=k; for (int i=L[belong[y]];i<=R[belong[y]];i++) d[i]=a[i]; sort(d+L[belong[y]],d+R[belong[y]]+1); for (int i=belong[x]+1;i<=belong[y]-1;i++) add[i]+=k;//使用lazy tag。因为整块内元素相对大小不变,所以无需进行重新排序。 }
3.查询
如果是查询区间和,那么直接$ans+=sum[i]+add[i]*(R[i]-L[i]+1)$即可。如果是进行大小的查询,因为块内我们已经排过序,元素满足单调性,所以我们可以进行二分查找。
跟修改块一样,我们同样是暴力处理边边角角,对于整块二分查找。
代码如下(以洛谷P2801为例):
inline void query() { int ans=0; if (belong[x]==belong[y])//特判 { for (int i=x;i<=y;i++) if (add[belong[x]]+a[i]>=k) ans++; printf("%d\n",ans); return; } for (int i=x;i<=R[belong[x]];i++) if (add[belong[x]]+a[i]>=k) ans++;//暴力 for (int i=L[belong[y]];i<=y;i++) if (add[belong[y]]+a[i]>=k) ans++; for (int i=belong[x]+1;i<=belong[y]-1;i++) { int ll=L[i],rr=R[i],res=0; while(ll<=rr)//二分,其元素满足单调性 { int mid=(ll+rr)/2; if (add[i]+d[mid]>=k) rr=mid-1,res=R[i]-mid+1; else ll=mid+1; } ans+=res; } printf("%d\n",ans); }
4.完整代码
这里以洛谷P2801为例。这题有一个小细节,我们应该开两个数组:操作数组和原始数组。因为排序会把原有元素的顺序打乱,所以我们在暴力处理边边角角的时候要用到原始数组。对于整块处理因为是整体处理,所以直接用操作数组即可。
代码:
#include<bits/stdc++.h> using namespace std; int a[1000005],d[1000005],L[1005],R[1005],belong[1000005],add[1005]; int n,q,block,tot,x,y,k; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void build() { block=sqrt(n);tot=n/block; if (n%block) tot++; for (int i=1;i<=n;i++) belong[i]=(i-1)/block+1,d[i]=a[i]; for (int i=1;i<=tot;i++) L[i]=(i-1)*block+1,R[i]=i*block; R[tot]=n; for (int i=1;i<=tot;i++) sort(d+L[i],d+R[i]+1); } inline void update() { if (belong[x]==belong[y]) { for (int i=x;i<=y;i++) a[i]+=k; for (int i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); return; } for (int i=x;i<=R[belong[x]];i++) a[i]+=k; for (int i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); for (int i=L[belong[y]];i<=y;i++) a[i]+=k; for (int i=L[belong[y]];i<=R[belong[y]];i++) d[i]=a[i]; sort(d+L[belong[y]],d+R[belong[y]]+1); for (int i=belong[x]+1;i<=belong[y]-1;i++) add[i]+=k; } inline void query() { int ans=0; if (belong[x]==belong[y]) { for (int i=x;i<=y;i++) if (add[belong[x]]+a[i]>=k) ans++; printf("%d\n",ans); return; } for (int i=x;i<=R[belong[x]];i++) if (add[belong[x]]+a[i]>=k) ans++; for (int i=L[belong[y]];i<=y;i++) if (add[belong[y]]+a[i]>=k) ans++; for (int i=belong[x]+1;i<=belong[y]-1;i++) { int ll=L[i],rr=R[i],res=0; while(ll<=rr) { int mid=(ll+rr)/2; if (add[i]+d[mid]>=k) rr=mid-1,res=R[i]-mid+1; else ll=mid+1; } ans+=res; } printf("%d\n",ans); } signed main() { n=read(),q=read(); for (int i=1;i<=n;i++) a[i]=read(); build(); for (int i=1;i<=q;i++) { char c;cin>>c; x=read(),y=read(),k=read(); if (c=='M') update(); else query(); } return 0; }