引入 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]); //将l块中的元素重新排序
if(pos[l]!=pos[r]){ //同理,处理r的边界段
for(int i=(pos[r]-1)*m+1;i<=r;i++) a[i]+=x;
changes(pos[r]); //将r块中的元素重新排序
} for(int i=pos[l]+1;i<=pos[r]-1;i++) tag[i]+=x;
}
int querys(int l,int r,int x){
int anss=0; //↓↓l,r同段 或者 先处理l的边界段
for(int i=l;i<=min(r,pos[l]*m);i++)
if(a[i]+tag[pos[l]]<x) anss++; //直接枚举统计
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(int i=(pos[r]-1)*m+1;i<=r;i++)
if(a[i]+tag[pos[r]]<x) anss++;
for(int i=pos[l]+1;i<=pos[r]-1;i++)
anss+=lower_bound(v[i].begin(),v[i].end(),x-tag[i])-v[i].begin();
return anss; //↑↑lower_bound返回第一个大于等于x的位置,省略了位置+1和答案-1
}
int main(/*hs_love_wjy*/){
scanf("%d",&n); m=sqrt(n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
pos[i]=(i-1)/m+1;
v[pos[i]].push_back(a[i]);
} for(int i=1;i<=pos[n];i++)
sort(v[i].begin(),v[i].end());
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); //题意中是查询比x^2小的个数
if(op==1) printf("%d\n",querys(l,r,x*x));
}
}
【练习3】区间修改,求区间内x的前驱
- 题意是求严格小于x的元素、所以要去重,需要使用set。
- 完整的块用lower_bound,不完整的块暴力修改。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习3】区间修改,求区间内比x小的最大元素。*/
/*【分析】题意的意思是严格小于、所以要去重,需要使用set。
完整的块用lower_bound,不完整的块暴力修改。*/
int n,m,pos[500019]; //pos[i]=(i-1)/m+1,即i属于哪一块
int a[500019],tag[500019];
//↑↑对整块使用tag标记,在询问的时候再计算tag的影响
set<int> s[519]; //set记录每块的元素(已分别排序去重)
//↓↓每块标记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++)
s[pos[i]].erase(a[i]),a[i]+=x,s[pos[i]].insert(a[i]);
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(int i=(pos[r]-1)*m+1;i<=r;i++)
s[pos[i]].erase(a[i]),a[i]+=x,s[pos[i]].insert(a[i]);
for(int i=pos[l]+1;i<=pos[r]-1;i++) tag[i]+=x;
}
int querys(int l,int r,int x){
int maxx=-1; //↓↓l,r同段 或者 先处理l的边界段
for(int i=l;i<=min(r,pos[l]*m);i++)
if(a[i]+tag[pos[l]]<x) maxx=max(maxx,a[i]+tag[pos[l]]);
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(int i=(pos[r]-1)*m+1;i<=r;i++)
if(a[i]+tag[pos[r]]<x) maxx=max(maxx,a[i]+tag[pos[r]]);
for(int i=pos[l]+1;i<=pos[r]-1;i++){
set<int>::iterator it=s[i].lower_bound(x-tag[i]);
if(it==s[i].begin()) continue; //没有比它小的
it--; maxx=max(maxx,*it+tag[i]);
} return maxx;
}
int main(/*hs_love_wjy*/){
scanf("%d",&n); m=sqrt(n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
pos[i]=(i-1)/m+1;
s[pos[i]].insert(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",querys(l,r,x));
}
}
【练习4】区间修改,区间求和
- 和单点查询类似,用sum数组记录每块的元素和即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习4】区间修改,区间求和。*/
/*【分析】和单点查询类似,用sum数组记录每块的元素和即可。*/
ll n,m,pos[500019]; //pos[i]=(i-1)/m+1,即i属于哪一块
ll a[500019],tag[500019],sum[500019];
//↑↑对整块使用tag标记,在询问的时候再计算tag的影响
//↓↓每块标记tag,剩下的l、r两个边界块直接修改
void adds(ll l,ll r,ll x){ //l,r同段 或者 先处理l的边界段
for(ll i=l;i<=min(r,pos[l]*m);i++) a[i]+=x,sum[pos[l]]+=x;
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(ll i=(pos[r]-1)*m+1;i<=r;i++) a[i]+=x,sum[pos[r]]+=x;
for(ll i=pos[l]+1;i<=pos[r]-1;i++) tag[i]+=x;
}
ll querys(ll l,ll r){
ll anss=0; //↓↓l,r同段 或者 先处理l的边界段
for(ll i=l;i<=min(r,pos[l]*m);i++) anss+=a[i]+tag[pos[l]];
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(ll i=(pos[r]-1)*m+1;i<=r;i++) anss+=a[i]+tag[pos[r]];
for(ll i=pos[l]+1;i<=pos[r]-1;i++) anss+=sum[i]+tag[i]*m;
return anss; //返回区间和
}
int main(/*hs_love_wjy*/){
scanf("%lld",&n); m=sqrt(n);
for(ll i=1;i<=n;i++) scanf("%lld",&a[i]),
pos[i]=(i-1)/m+1,sum[pos[i]]+=a[i];
for(ll i=1,op,l,r,x;i<=n;i++){
scanf("%lld%lld%lld%lld",&op,&l,&r,&x);
if(op==0) adds(l,r,x); //↓↓要求%(x+1)
if(op==1) printf("%lld\n",querys(l,r)%(x+1));
}
}
【练习5】区间开方取整,区间求和
- 这个问题就有点玄学了...想不到就懵了...
- 2^31开5次平方就得到1了,所以操作次数>5次之后区间内就都是1了。
- 用okk[i]数组标记第i块内的数是不是全都小于等于1了;如果不是就整块更新。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习5】区间开方取整,区间求和。*/
/*【分析】这个问题就有点玄学了...想不到就懵了...
因为最大值是2^31,它开5次平方就得到1了,所以操作次数>5次之后区间内就都是1了。
用okk[i]数组标记第i块内的数是不是全都小于等于1了;如果不是就整块更新。*/
ll n,m,pos[500019]; //pos[i]=(i-1)/m+1,即i属于哪一块
ll a[500019],tag[500019],sum[500019]; //【此题不用tag】
bool okk[500019]; //标记块内是不是全都<=1
//↓↓每块标记tag,剩下的l、r两个边界块直接修改
void sqrt_(ll num){ //整段的开方
if(okk[num]) return;
okk[num]=true,sum[num]=0;
for(ll i=(num-1)*m+1;i<=min(num*m,n);i++){
a[i]=sqrt(a[i]),sum[num]+=a[i];
if(a[i]>1) okk[num]=false;
}
}
void adds(ll l,ll r){
if(okk[pos[l]]==false){ //l,r同段 或者 先处理l的边界段
for(ll i=l;i<=min(r,pos[l]*m);i++)
sum[pos[l]]-=a[i],a[i]=sqrt(a[i]),sum[pos[l]]+=a[i];
okk[pos[l]]=true; //判断l的边界段是否全部变为<=1
for(ll i=(pos[l]-1)*m+1;i<=min(n,pos[l]*m);i++)
if(a[i]>1){ okk[pos[l]]=false; break; }
} if(pos[l]!=pos[r]&&okk[pos[r]]==false){ //同理,处理r的边界段
for(ll i=(pos[r]-1)*m+1;i<=r;i++)
sum[pos[r]]-=a[i],a[i]=sqrt(a[i]),sum[pos[r]]+=a[i];
okk[pos[r]]=true; //判断r的边界段是否全部变为<=1
for(ll i=(pos[r]-1)*m+1;i<=min(n,pos[r]*m);i++)
if(a[i]>1){ okk[pos[r]]=false; break; }
} for(ll i=pos[l]+1;i<=pos[r]-1;i++) sqrt_(i);
}
ll querys(ll l,ll r){
ll anss=0; //↓↓l,r同段 或者 先处理l的边界段
for(ll i=l;i<=min(r,pos[l]*m);i++) anss+=a[i];
if(pos[l]!=pos[r]) //同理,处理r的边界段
for(ll i=(pos[r]-1)*m+1;i<=r;i++) anss+=a[i];
for(ll i=pos[l]+1;i<=pos[r]-1;i++) anss+=sum[i];
return anss; //返回区间和
}
int main(/*hs_love_wjy*/){
memset(okk,false,sizeof(okk));
scanf("%lld",&n); m=sqrt(n);
for(ll i=1;i<=n;i++) scanf("%lld",&a[i]),
pos[i]=(i-1)/m+1,sum[pos[i]]+=a[i];
for(ll i=1,op,l,r,x;i<=n;i++){
scanf("%lld%lld%lld%lld",&op,&l,&r,&x);
if(op==0) adds(l,r); //不要怀疑,x就是没有用的
if(op==1) printf("%lld\n",querys(l,r));
}
}
【练习6】单点插入,单点询问
- 用a[i][j]记录第i块的第j个数,len[i]记录第i块有多少个数。
- 暴力找到插入的点应在的块,暴力把此块中在此位置后面所有数的id往后挪。
- 所以当某个块的太大时、就重新分块。此时先用b数组分好块,在复制到a中。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习6】单点插入,单点询问。*/
/*【分析】用a[i][j]记录第i块的第j个数,len[i]记录第i块有多少个数。
暴力找到插入的点应在的块,暴力把此块中在此位置后面所有数的id往后挪。
所以当某个块的太大时、就重新分块。此时先用b数组分好块,在复制到a中。*/
int a[1019][1019],b[1019][1019],len[1019]; //b:a的转存数组;len:每一块的长度
int n,N,add_num=0,m; //add_num:新增点数,m:新总长,N:每块的大小
void rebuild(){ //重新分块
int now=0,k=(m-1)/N+1,lens=0;
m+=N; add_num=0; N=sqrt(m); //新加了N个元素
for(int i=1;i<=k;i++){ //原块数
for(int j=1;j<=len[i];j++){ //每个数
now++; int kk=(now-1)/N+1;
b[kk][++lens]=a[i][j]; a[i][j]=0;
if(lens==N) lens=0;
}
} k=(m-1)/N+1; //新的总块数
for(int i=1;i<k;i++){ //前方的所有满的块数
len[i]=N; for(int j=1;j<=len[i];j++) a[i][j]=b[i][j];
} if(m%N==0) len[k]=N; else len[k]=m-N*N;
for(int j=1;j<=len[k];j++) a[k][j]=b[k][j]; //复制
}
int main() {
scanf("%d",&n);N=sqrt(n); m=n;
for(int i=1;i<=n;i++){ int s,k=(i-1)/N+1;
scanf("%d",&s),a[k][++len[k]]=s; //直接存到分块数组中
} for(int i=1,op,l,r,x;i<=n;i++){
scanf("%d%d%d%d",&op,&l,&r,&x);
if(op==0){ add_num++; int now=0,t;
for(t=1;t<=N;t++){ //修改l位置的数
if(now+len[t]>=l) break; //找到所在块的编号
else now+=len[t]; //在后面的块中
} len[t]++; l=l-now; //第t块的len++,寻找在块中的具体位置
for(int i=len[t];i>l;i--) a[t][i]=a[t][i-1]; //后方数字后移
a[t][l]=r; if(add_num==N) rebuild(); //如果增加总数达到N,重新分块
} else{ int t,now=0;
for(t=1;t<=N;t++){ //查询r位置的数
if(now+len[t]>=r) break; //找到所在块的编号
else now+=len[t]; //在后面的块中
} printf("%d\n",a[t][r-now]); //直接输出
}
}
}
【练习7】区间乘法,区间加法,单点查询
- 维护乘法标记tag_mul和加法标记tag_add。
- 乘法可以看做是:a*tag_mul+0;加法可以看做是:a*1+tag_add。
- 每次新加入一个操作:加法时,直接将整块的tag_add[i]+x;
- 乘法时,将整快的tag_mul[i]*x,tag_add[i]*x。
- 对于左右的零散块,将块中所有值都计算出来,清空标记。
- 计算总的标记(单点查询时)就是:a*tag_mul+tag_add。
#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;
/*【分块入门练习7】区间乘法,区间加法,单点查询。*/
/*【分析】维护乘法标记tag_mul和加法标记tag_add。
乘法可以看做是:a*tag_mul+0;加法可以看做是:a*1+tag_add。
每次新加入一个操作:加法时,直接将整块的tag_add[i]+x;
乘法时,将整快的tag_mul[i]*x,tag_add[i]*x。
对于左右的零散块,将每块的所有值都计算出来,清空标记,暴力修改。
计算总的标记就是:a*tag_mul+tag_add。*/
const int mod=10007;
int n,m,tol,T,a[500019],pos[500019];
int tag_add[500019],tag_mul[500019];
void add(int l,int r,int x){
for(int i=(pos[l]-1)*m+1;i<=min(n,pos[l]*m);i++)
a[i]=(a[i]*tag_mul[pos[i]]%mod+tag_add[pos[i]])%mod;
tag_add[pos[l]]=0,tag_mul[pos[l]]=1; //↑↑计算这一块先前的所有更新
for(int i=l;i<=min(r,pos[l]*m);i++) a[i]=(a[i]+x)%mod; //更新a[i]
if(pos[l]==pos[r]) return;
for(int i=(pos[r]-1)*m+1;i<=min(n,pos[r]*m);i++)
a[i]=(a[i]*tag_mul[pos[i]]%mod+tag_add[pos[i]])%mod;
tag_add[pos[r]]=0,tag_mul[pos[r]]=1; //↑↑计算这一块先前的所有更新
for(int i=(pos[r]-1)*m+1;i<=r;i++) a[i]=(a[i]+x)%mod;
for(int i=pos[l]+1;i<=pos[r]-1;i++) tag_add[i]=(tag_add[i]+x)%mod;
}
void mul(int l,int r,int x){
for(int i=(pos[l]-1)*m+1;i<=min(n,pos[l]*m);i++)
a[i]=(a[i]*tag_mul[pos[i]]%mod+tag_add[pos[i]])%mod;
tag_add[pos[l]]=0,tag_mul[pos[l]]=1; //↑↑计算这一块先前的所有更新
for(int i=l;i<=min(r,pos[l]*m);i++) a[i]=(a[i]*x)%mod; //更新a[i]
if(pos[l]==pos[r]) return;
for(int i=(pos[r]-1)*m+1;i<=min(n,pos[r]*m);i++)
a[i]=(a[i]*tag_mul[pos[i]]%mod+tag_add[pos[i]])%mod;
tag_add[pos[r]]=0,tag_mul[pos[r]]=1; //↑↑计算这一块先前的所有更新
for(int i=(pos[r]-1)*m+1;i<=r;i++) a[i]=(a[i]*x)%mod;
for(int i=pos[l]+1;i<=pos[r]-1;i++) //注意:mul和add标记都要*x
tag_mul[i]=(tag_mul[i]*x)%mod,tag_add[i]=(tag_add[i]*x)%mod;
}
int main(){
scanf("%d",&n); m=sqrt(n);
for(int i=1;i<=n;i++) //预处理pos,初始化tag_mul
scanf("%d",&a[i]),pos[i]=(i-1)/m+1,tag_mul[i]=1;
for(int i=1,op,l,r,x;i<=n;i++){
scanf("%d%d%d%d",&op,&l,&r,&x);
if(op==0) add(l,r,x%mod); //加法
if(op==1) mul(l,r,x%mod); //乘法
if(op==2) printf("%d\n",(a[r]*tag_mul[pos[r]]%mod+tag_add[pos[r]]%mod)%mod);
} //查询r点现在的值
}
【练习8】区间询问,区间修改
- 题意:查询区间内有多少个x,并把区间内全部改成x。
- 类似开方的那个题。用okk数组记录是否块内每个数字都相同。
#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;
/*【分块入门练习8】区间询问,区间修改。*/
//题意:查询区间内有多少个x,并把区间内全部改成x。
/*【分析】类似开方的那个题。用okk数组记录是否块内每个数字都相同。*/
const int maxn=500019;
int n,m,a[maxn],pos[maxn];
int add[maxn]; bool okk[maxn];
int L(int num){ return (num-1)*m+1; }
int R(int num){ return min(n,num*m); }
int query(int l,int r,int x){
int ans=0; //查找在区间l,r内有多少个x
if(okk[pos[l]]){ if(add[pos[l]]==x) ans+=min(R(pos[l]),r)-l+1; }
else{ for(int i=l;i<=min(R(pos[l]),r);i++) ans+=(a[i]==x); }
if(pos[l]==pos[r]) return ans;
if(okk[pos[r]]){ if(add[pos[r]]==x) ans+=r-L(pos[r])+1;}
else{ for(int i=L(pos[r]);i<=r;i++) ans+=(a[i]==x); }
for(int i=pos[l]+1;i<=pos[r]-1;i++){
if(okk[i]){ if(add[i]==x) ans+=R(i)-L(i)+1; }
else{ for(int j=L(i);j<=R(i);j++) ans+=(a[j]==x); }
} return ans;
}
void change(int l,int r,int x){
if(okk[pos[l]]){ okk[pos[l]]=false;
for(int i=L(pos[l]);i<=R(pos[l]);i++) a[i]=add[pos[i]];
} for(int i=l;i<=min(r,R(pos[l]));i++) a[i]=x;
if(pos[l]==pos[r]) return;
if(okk[pos[r]]){ okk[pos[r]]=false;
for(int i=L(pos[r]);i<=R(pos[r]);i++) a[i]=add[pos[i]];
} for(int i=L(pos[r]);i<=r;i++) a[i]=x;
for(int i=pos[l]+1;i<=pos[r]-1;i++) add[i]=x,okk[i]=true;
}
int main(/*hs_love_wjy*/){
scanf("%d",&n),m=sqrt(n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),pos[i]=(i-1)/m+1;
for(int i=1,l,r,x;i<=n;i++){
scanf("%d%d%d",&l,&r,&x);
printf("%d\n",query(l,r,x)),change(l,r,x);
}
}
【练习9】查询区间最小众数
- 分块,预处理出从第i块到第j块的众数,用f[i][j]记录。
- 用vector来保存【每个数】的所有的出现位置,
- 求不完整的块上的每个数的出现次数,找出现次数最多的数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【分块入门练习9】查询区间最小众数。*/
/*【分析】分块,预处理出从第i块到第j块的众数,用f[i][j]记录。
用vector来保存【每个数】的所有的出现位置,对于不完整的块,
求每个数的出现次数,找最大的出现次数。*/
const int N=100019;
int n,m,a[N],pos[N],f[4021][4021],b[N];
int cnt[100019]; vector<int> v[N];
int L(int num){ return (num-1)*m+1; }
int R(int num){ return min(n,num*m); }
void init(int num){
int now_=0,ans=0;
memset(cnt,0,sizeof(cnt));
for(int i=L(num);i<=n;i++){
cnt[a[i]]++; if(cnt[a[i]]>ans){
now_=a[i],ans=cnt[a[i]];
} if(cnt[a[i]]==ans&&a[i]<now_){
now_=a[i],ans=cnt[a[i]]; //区间最小众数
} f[num][pos[i]]=now_; //每次都更新
} //↑↑预处理:从第i块到第j块(整块)的最小众数
}
int ask_(int l,int r,int x){ //v数组从前到后保存每个数字出现的[位置]
return upper_bound(v[x].begin(),v[x].end(),r)
-lower_bound(v[x].begin(),v[x].end(),l);
} //↑↑查找从位置l到位置r,数字x一共出现了多少次
int query(int l,int r){
int now=f[pos[l]+1][pos[r]-1],ans=ask_(l,r,now),ans_;
for(int i=l;i<=min(r,R(pos[l]));i++){
ans_=ask_(l,r,a[i]);
if(ans_>ans){ ans=ans_; now=a[i]; }
if(ans_==ans&&a[i]<now){ ans=ans_; now=a[i]; }
} if(pos[l]==pos[r]) return now;
for(int i=L(pos[r]);i<=r;i++){
ans_=ask_(l,r,a[i]);
if(ans_>ans){ ans=ans_; now=a[i]; }
if(ans_==ans&&a[i]<now){ ans=ans_; now=a[i]; }
} return now; //返回众数
}
int main(/*hs_love_wjy*/){
scanf("%d",&n),m=50; //据说50可以快很多...
for(int i=1;i<=n;i++) //↑↑反正sqrt(n)会tle...
scanf("%d",&a[i]),pos[i]=(i-1)/m+1,b[i]=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中存放的数字a[i]。只需要-b即可返回在b数组中的下标
v[a[i]].push_back(i); //把每个【数】出现的【位置】放入vector数组中
} for(int i=1;i<=pos[n];i++) init(i);
for(int i=1,l,r;i<=n;i++) //询问区间(i,j)的最小众数
scanf("%d%d",&l,&r),printf("%d\n",b[query(l,r)]);
}
——时间划过风的轨迹,那个少年,还在等你