1.什么是分块
说白了,分块就是优化的暴力。
例如,进行区间加法操作时,我们可以把序列分为若干个块,被全覆盖的块只需进行标记,再把没被全覆盖的进行暴力处理,进行大量大范围操作时区块相对暴力的优越性是显然的。
通常取每个块的大小为 N \sqrt N N,这样可以达到时空平衡,时间复杂度为 O ( ( N + Q ) ∗ log N ) O((N+Q) * \log N) O((N+Q)∗logN) 。
2.分块的作用与优缺点
与树状数组和线段树等数据结构的作用一样,分块是用来维护一个大区间的各种操作和查询。
与前两种做法相比,分块的优点就是直观,简单易懂,而且可拓展性强,可以维护很多类型的操作。
但是分块的缺点也很明显,就是效率较低,相比线段树要慢很多,而且复杂度具有随数据情况而变的不确定性,所以能使用其他优秀的做法尽量还是不要用分块,当然前提是保证正确。
3.分块的一般步骤
以分块入门1为例
#include<bits/stdc++.h>
using namespace std;
int n,m,len;
int pos[101000];
//个人习惯,可以用函数代替pos数组
int a[101000];
int add[400];
//加法标记数组,一个块只需要一个
void ADD(int l,int r,int val){
int p=pos[l],q=pos[r];//访问两端点所在块的编号
if(p==q){
//在同一块内暴力更新就好
for(int i=l;i<=r;i++) a[i]+=val;
}
else{
//先把p~q中被完全覆盖的块打上加法标记
for(int i=p+1;i<q;i++) add[i]+=val;
//对于左右两端没被完全覆盖的地方暴力更新
for(int i=l;i<=p*len;i++) a[i]+=val;
for(int i=(q-1)*len+1;i<=r;i++) a[i]+=val;
//个人习惯计算每一块的左右端点,可以提前预处理每一块的左右端点,存在数组中访问
}
}
int get(int id){
return a[id]+add[pos[id]];
}
int main(){
scanf("%d",&n);m=n;len=sqrt(n);//默认块长为sqrt(n)
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[i]=(i-1)/len+1;//预处理每个位置所在块的编号
}
while(m--){
int op,l,r,c;
scanf("%d%d%d%d",&op,&l,&r,&c);
if(op==0) ADD(l,r,c);
else printf("%d\n",get(r));
}
return 0;
}
对于一些特殊的题目,进行的操作不能简单的处理。
这里以分块入门8为例
这道题的操作时把整个区间变为某个数字,显然想上一题那样累加标记是无法完成的,需要通过其他方法维护标记数组。
#include<bits/stdc++.h>
using namespace std;
//
int n,m,len;
int pos[101000];
int a[101000];
int now[400];
//表示现在这一块整体是哪个数字,如果块内数字不同,则该块值为-1。
int get(int l,int r,int c){
int p=pos[l],q=pos[r];
if(p==q){
//在同一块内,暴力查询。
int cnt=0;
//该块的标记就是目标值,直接统计
if(now[p]==c) return r-l+1;
//否则就暴力统计有多少个目标值
else if(now[p]==-1) for(int i=l;i<=r;i++) cnt+=a[i]==c?1:0;
return cnt;
}
else {
int cnt=0;
for(int i=p+1;i<=q-1;i++) {
if(now[i]==c) cnt+=len;
//标记值为目标值,统计上该块内的所有元素
else if(now[i]==-1) {
for(int k=(i-1)*len+1;k<=i*len;k++){
cnt+=a[k]==c?1:0;
}
//如果该块内不是统一的,暴力查找。
}
//如果块内统一且标记不是目标值,直接忽略。
}
//与上一步类似的未完全覆盖块的统计
if(now[p]==c) cnt+=p*len-l+1;
else if(now[p]==-1) for(int i=l;i<=p*len;i++) cnt+=a[i]==c?1:0;
if(now[q]==c) cnt+=r-(q-1)*len;
else if(now[q]==-1) for(int i=(q-1)*len+1;i<=r;i++) cnt+=a[i]==c?1:0;
return cnt;
}
}
void change(int l,int r,int c){
int p=pos[l],q=pos[r];
if(p==q){
//在同一块内暴力更新
if(now[p]==-1){
//该块内的数不是统一的,就直接更新。
for(int i=l;i<=r;i++) a[i]=c;
}
else if(now[p]!=c){
//如果该块有标记且不是目标修改值,把块内其他元素赋值为标记,再把要被修改的地方进行修改
for(int i=(p-1)*len+1;i<=p*len;i++) a[i]=now[p];
now[p]=-1;
for(int i=l;i<=r;i++) a[i]=c;
}
//如果本来这一块的标记就是目标修改值,就没必要进行任何操作。
//否则,由于块内部分元素被修改,该块的标记打为-1。
}
else{
for(int i=p+1;i<=q-1;i++) now[i]=c;
//修改被全覆盖的块
//接下来的操作以上面的相同,都是对没有被完全覆盖的块进行操作。
if(now[p]==-1){
for(int i=l;i<=p*len;i++) a[i]=c;
}
else if(now[p]!=c){
for(int i=(p-1)*len+1;i<=p*len;i++) a[i]=now[p];
now[p]=-1;
for(int i=l;i<=p*len;i++) a[i]=c;
}
if(now[q]==-1){
for(int i=(q-1)*len+1;i<=r;i++) a[i]=c;
}
else if(now[q]!=c){
for(int i=(q-1)*len+1;i<=q*len;i++) a[i]=now[q];
now[q]=-1;
for(int i=(q-1)*len+1;i<=r;i++) a[i]=c;
}
}
}
int main(){
scanf("%d",&n);m=n;len=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[i]=(i-1)/len+1;
}
for(int i=1;i<=pos[n];i++) now[i]=-1;
//这里巧用了第n个位置的所在块编号,避免用整除判断来计算总块数。
int l,r,c;
while(m--){
scanf("%d%d%d",&l,&r,&c);
printf("%d\n",get(l,r,c));
change(l,r,c);
}
return 0;
}
有些题目要统计的数值很特殊,所以需要拓展标记数组。
这里以蒲公英为例。
如果题目不强制在线,那么可以用莫队处理,但是不能用莫队就只能用分块。
这道题需要求区间众数,而区间众数不具有可加性,所以我们这样考虑:
1.先统计处每个块内数字出现的次数。
2.再预处理出任意个连续块中的众数。
3.询问时,先给出预处理出的完整块的答案,接着枚举左右未完全覆盖区间的数,更新众数。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int len;
int a[101000];
int pos[101000];
int val[101000],cnt;
int qian[400][101000];
int num[400][400];
int cun[101000];
void lisan(){
//注意离散化
sort(val+1,val+1+n);
cnt=unique(val+1,val+1+n)-val-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(val+1,val+cnt+1,a[i])-val;
}
int main(){
scanf("%d%d",&n,&m);
len=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos[i]=(i-1)/len+1,val[i]=a[i];
lisan();
for(int i=1;i<=pos[n];i++) {
for(int j=(i-1)*len+1;j<=min(i*len,n);j++)
//1.统计
qian[i][a[j]]++;
for(int j=1;j<=cnt;j++)
//2.预处理
qian[i][j]+=qian[i-1][j];
}
for(int i=1;i<=pos[n];i++){
for(int j=i;j<=pos[n];j++){
int minn=num[i][j-1];
for(int k=(j-1)*len+1;k<=min(n,len*j);k++){
if((qian[j][a[k]]-qian[i-1][a[k]]>qian[j][minn]-qian[i-1][minn])||(qian[j][a[k]]-qian[i-1][a[k]]==qian[j][minn]-qian[i-1][minn]&&a[k]<minn)) minn=a[k];
}
num[i][j]=minn;
}
}
int mod=0;
while(m--){
int ll,rr;
scanf("%d%d",&ll,&rr);
ll=(ll+mod-1)%n+1,rr=(rr+mod-1)%n+1;
if(ll>rr) swap(ll,rr);
int p=pos[ll],q=pos[rr];
int minn=0;
if(q-p<=1) {
for(int i=ll;i<=rr;i++) cun[a[i]]++;
for(int i=ll;i<=rr;i++) if(cun[a[i]]>cun[minn]||(cun[a[i]]==cun[minn]&&a[i]<minn)) minn=a[i];
for(int i=ll;i<=rr;i++) cun[a[i]]=0;
}
else {
for(int i=ll;i<=len*p;i++) cun[a[i]]++;
for(int i=len*(q-1)+1;i<=rr;i++) cun[a[i]]++;
minn=num[p+1][q-1];
for(int i=ll;i<=len*p;i++){
int k=cun[minn]+qian[q-1][minn]-qian[p][minn],now=cun[a[i]]+qian[q-1][a[i]]-qian[p][a[i]];
if(now>k||(now==k&&minn>a[i])) minn=a[i];
}
for(int i=len*(q-1)+1;i<=rr;i++)
{
int k=cun[minn]+qian[q-1][minn]-qian[p][minn],now=cun[a[i]]+qian[q-1][a[i]]-qian[p][a[i]];
if(now>k||(now==k&&minn>a[i])) minn=a[i];
}
for(int i=ll;i<=len*p;i++) cun[a[i]]=0;
for(int i=len*(q-1)+1;i<=rr;i++) cun[a[i]]=0;
}
printf("%d\n",val[minn]);
mod=val[minn];
}
return 0;
}
通过这几道例题不难发现分块是有一定模板的,这是因为分块的思路都是如下这样的:
1.确定要统计的信息的性质。
2.针对性质设计标记。
3.设计转移以及对于未完全覆盖块的处理。
4.注意事项
1.访问到最后一块 x x x时,注意右端点为 m i n ( x ∗ l e n , n ) min(x*len,n) min(x∗len,n)。
2.记得数组开大一点。
3.做区间混合运算注意优先级。
5.拓展:莫队算法
1.什么是莫队
如果说分块算法是把待询问区间分块,那么莫队算法则是把所有的询问分块。
我们把所有的询问离线后,按照端点所在块排序,从左到右,依次把已经答案的区间转移,这样的时间复杂度 O ( Q ∗ ( N ) ) O(Q * \sqrt(N)) O(Q∗(N))。
不难看出,能用莫队的题必须具有的特性是询问可离线,强制在线的题不能用莫队,所以也是有一定局限性的。
2.莫队的一般步骤
以HH的项链为例
显然暴力去询问只会得到香葱炒鸡蛋,所以必须上莫队。
#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int pos[501000];
void pre(){
for(int i=1;i<=n;i++) pos[i]=i%m==0?i/m:i/m+1;
}
int a[501000];
int b[501000],tot=0;
int c[501000];
void lisan(){
//记得离散化
sort(c+1,c+n+1);
for(int i=1;i<=n;i++)
if(i==1||c[i]!=c[i-1])
b[++tot]=c[i];
for(int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+tot+1,a[i])-b;
}
struct node{
int l,r,id;
}ask[501000];
int now=0;
bool mycmp(node x,node y){
return (pos[x.l]<pos[y.l])||(pos[x.l]==pos[y.l]&&pos[x.r]<pos[y.r]);
//按照左右端点所在块编号排序
}
int num[501000];
void add(int id){
num[a[id]]++;
if(num[a[id]]==1) now++;
}
void del(int id){
num[a[id]]--;
if(num[a[id]]==0) now--;
}
int ans[501000];
int main(){
scanf("%d",&n);
m=sqrt(n);pre();
for(int i=1;i<=n;i++) scanf("%d",&a[i]),c[i]=a[i];
lisan();
scanf("%d",&q);
for(int i=1;i<=q;i++) scanf("%d%d",&ask[i].l,&ask[i].r),ask[i].id=i;
sort(ask+1,ask+1+q,mycmp);
for(int i=1;i<=n;i++) {
num[a[i]]++;
if(num[a[i]]==1) now++;
//相当于1~n的add操作
}
int ll=1,rr=n;
for(int i=1;i<=q;i++) {
//移动当前选择区间的左右端点,并增删元素
while(ll<ask[i].l) del(ll),ll++;
while(ask[i].l<ll) ll--,add(ll);
while(rr>ask[i].r) del(rr),rr--;
while(ask[i].r>rr) rr++,add(rr);
ans[ask[i].id]=now;
}
for(int i=1;i<=q;i++) printf("%d\n",ans[i]);
return 0;
}
总结下来,莫队的基本步骤:
1.离线询问,按照块编号排序。
2.先统计整个区间全部答案,再进行区间端点的转移,区间元素的增删。
3.按照询问顺序输出答案。
3.注意事项
1.使用莫队必须保证询问可离线。
2.记得移动端点时是否最终的区间包含的端点的元素。
建议格式:
while(ll<ask[i].l) del(ll),ll++;
while(ask[i].l<ll) ll--,add(ll);
while(rr>ask[i].r) del(rr),rr--;
while(ask[i].r>rr) rr++,add(rr);
3.最后输出答案要按照询问顺序排序后输出。
6.例题
loj:分块入门1~9。
传送门:
1 2 3
4 5 6
7 8 9
磁力块(分块+队列)
小Z的袜子 (莫队+gcd)
XOR and Favorite Number (莫队+异或+逆向思维)