分块与莫队
不沾树的博客变短了好多。
分块
这道题显然可以使用线段树乱搞过去,不过为了给主角面子我们假设我们不会做。
对于一些难以使用数据结构维护答案的序列问题,我们考虑暴力。但是暴力太慢了,于是人们提出了分块。
分块,就是把序列分成许多的小段,通过一些神秘的处理实现优化暴力。
并且应当保证,整块内的答案可以
考虑均值不等式,当且仅当块数
考虑用分块解决线段树问题。
分块的区间操作也参考了线段树的
有一个技巧:不建议在分块中使用下放上传操作,而是汇总答案对每个部分都加上
#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
#define MAXM 320
using namespace std;
int n,q;
int num,tot,f[MAXN],siz[MAXM];
int a[MAXN],flag[MAXM],sum[MAXM];
inline void modify(int l,int r,int val){
for(int i=l;i<=min(r,f[l]*num);i++)a[i]+=val,sum[f[i]]+=val;
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)a[i]+=val,sum[f[i]]+=val;
}
for(int i=f[l]+1;i<=f[r]-1;i++)flag[i]+=val;
}
inline int getans(int l,int r){
int res=0;
for(int i=l;i<=min(r,f[l]*num);i++)res+=a[i]+flag[f[i]];
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)res+=a[i]+flag[f[i]];
}
for(int i=f[l]+1;i<=f[r]-1;i++)res+=sum[i]+flag[i]*siz[i];
return res;
}
signed main(){
scanf("%lld%lld",&n,&q);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1,sum[f[i]]+=a[i],++siz[f[i]];
for(int id=1,opt,l,r,k;id<=q;id++){
scanf("%lld%lld%lld",&opt,&l,&r);
if(opt==1){
scanf("%lld",&k);
modify(l,r,k);
}
else printf("%lld\n",getans(l,r));
}
return 0;
}
在数据随机的情况下,分块可能会体现更优秀的时间复杂度。
分块的思想简单粗暴,但是如何有效地设计并处理分块来有效降低复杂度与其他技巧的兼容性极强,并不好想,这导致分块题目难度上限高。
这道题用动态树解决,但是我们不会动态树,使用分块解决。
考虑将序列分成
这样统计答案就是
这样修改也是
如何处理块内有多少元素大于
考虑修改,修改操作只会对边角所在的块造成影响,需要暴力重造两个块,复杂度
总复杂度
注意最后的块可能是不完整的,我们不想处理块的大小导致的巨量细节,使用 vector 能处理地稍微方便一些。
#include<bits/stdc++.h>
#define MAXN 1000005
#define MAXM 1005
#define int long long
using namespace std;
int n,q;
int num,tot,f[MAXN];
vector<int>val[MAXM];
int a[MAXN],block[MAXM];
inline void reset(int id){
val[id].clear();
for(int i=(id-1)*num+1;i<=min(n,id*num);i++)val[id].push_back(a[i]);
sort(val[id].begin(),val[id].end());
}
signed main(){
scanf("%lld%lld",&n,&q);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
val[f[i]].push_back(a[i]);
}
for(int i=1;i<=tot;i++)sort(val[i].begin(),val[i].end());
for(int id=1,l,r,v;id<=q;id++){
char opt[3];
scanf("%s",opt+1);
scanf("%lld%lld%lld",&l,&r,&v);
if(opt[1]=='M'){
for(int i=l;i<=min(r,f[l]*num);i++)
a[i]+=v;
reset(f[l]);
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)
a[i]+=v;
reset(f[r]);
}
for(int i=f[l]+1;i<=f[r]-1;i++)
block[i]+=v;
}
else{
int res=0;
for(int i=l;i<=min(r,f[l]*num);i++)if(a[i]+block[f[i]]>=v)++res;
if(f[l]!=f[r])for(int i=(f[r]-1)*num+1;i<=r;i++)if(a[i]+block[f[i]]>=v)++res;
for(int i=f[l]+1;i<=f[r]-1;i++){
int xx=v-block[i];
res+=val[i].size()-(lower_bound(val[i].begin(),val[i].end(),xx)-val[i].begin());
}
printf("%lld\n",res);
}
}
return 0;
}
一般题目中出现“输入是加密的”说明要求强制在线(不然怎么输入啊)。特地提出“l>r时交换”说明数据是随机的。这时候分块被卡的概率会很低。
这道题使用邪道做法:前缀和块元素个数,暴力扫块内元素,复杂度
现在提供这种题的正经做法。
一般采用
不过分块题的答案肯定不是可以简单合并的,在这道题中边角的数与块内的数一旦有重合就会影响总个数。
考虑再使用
对于一段区间
即原本算在
即只在边角出现的偶数个元素需要加上
即边角与块内个数都为奇数的元素要加上,因为合起来就是偶数了。
并且处于复杂度我们不能枚举元素种类,注意到大部分块内的答案是不变的,只有边角
所以在扫边角时将可能对答案有影响的元素统计并入栈。处理块答案时再拿出来就行,
不过如何预处理?
显然预处理最多只能是
但是我们发现总复杂度为
当且仅当
不过这还不是最优解,因为在处理问题时复杂度会有常数,所以会设计出各种时空配置的分块。
这里只提供初步优化。
#include<bits/stdc++.h>
#define MAXN 100005
#define MAXM 1005
using namespace std;
int n,c,m;
int num,tot;
int f[MAXN];
int sum[MAXM][MAXN],block[MAXM][MAXM];
int a[MAXN],ans;
int val[MAXN],stac[MAXN],top;
int main(){
scanf("%d%d%d",&n,&c,&m);
while(num*num<n*n/m)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=tot;i++){
int cnt=0;
for(int j=(i-1)*num+1;j<=n;j++){
++sum[i][a[j]];
if((sum[i][a[j]]%2==1)&&(sum[i][a[j]]>1))cnt--;
else if(sum[i][a[j]]%2==0)cnt++;
if(f[j]!=f[j+1])block[i][f[j]]=cnt;
}
}
for(int id=1,l,r;id<=m;id++){
scanf("%d%d",&l,&r);
l=(l+ans)%n+1,r=(r+ans)%n+1;
if(l>r)swap(l,r);
ans=0;
if(f[r]==f[l]){
for(int i=l;i<=r;i++){
++val[a[i]];
stac[++top]=a[i];
}
while(top){
if(val[stac[top]]){
if(val[stac[top]]%2==0)++ans;
val[stac[top]]=0;
}
--top;
}
printf("%d\n",ans);
continue;
}
if(f[l]+1<=f[r]-1)ans=block[f[l]+1][f[r]-1];
for(int i=l;i<=f[l]*num;i++){
++val[a[i]];
stac[++top]=a[i];
}
for(int i=(f[r]-1)*num+1;i<=r;i++){
++val[a[i]];
stac[++top]=a[i];
}
while(top){
int v=stac[top];
if(val[v]){
if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==0)&&(val[v]%2==1))--ans;
if(((sum[f[l]+1][v]-sum[f[r]][v])==0)&&((val[v]%2)==0))++ans;
if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==1)&&(val[v]%2==1))++ans;
val[v]=0;
}
--top;
}
printf("%d\n",ans);
}
return 0;
}
对于蒲公英那道题,做法是相似的,维护区间众数与数字前后缀和,显然只有边角数字会影响答案,扫一遍边角数字重新统计即可。
求
考虑
统计元素个数时,碎块元素数
为了少枚举一次颜色,单个增加时有:
设块长为
不过还是可以不等式的。
为了方便处理令
#include<bits/stdc++.h>
#define MAXN 20005
#define MAXM 50
#define N 50005
using namespace std;
int n,m,q;
int num,tot;
int a[N],f[N];
int val[MAXN];
int sum[MAXM][MAXM][MAXN],block[MAXM][MAXM][MAXN];
int ans,top,stac[N];
int main(){
scanf("%d%d%d",&n,&m,&q);
while((long long)num*num*num<=(long long)n*n)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=tot;i++){
for(int j=i;j<=tot;j++){
for(int k=(j-1)*num+1;k<=min(n,j*num);k++)++val[a[k]];
for(int k=1;k<=m;k++)sum[i][j][k]=val[k],block[i][j][k]=val[k]*val[k]+block[i][j][k-1];
}
for(int j=1;j<=m;j++)val[j]=0;
}
for(int id=1,l,r,A,B;id<=q;id++){
scanf("%d%d%d%d",&l,&r,&A,&B);
if(ans)l^=ans,r^=ans,A^=ans,B^=ans;
ans=0;
if(l>r)swap(l,r);
if(A>B)swap(A,B);
if(f[r]==f[l]){
int res=0;
for(int i=l;i<=r;i++){
if(a[i]>=A&&a[i]<=B)res+=val[a[i]]*2+1,++val[a[i]],stac[++top]=a[i];
}
while(top){
if(val[stac[top]])val[stac[top]]=0;
--top;
}
printf("%d\n",res);
ans=res;
continue;
}
ans=block[f[l]+1][f[r]-1][B]-block[f[l]+1][f[r]-1][A-1];
for(int i=l;i<=min(r,f[l]*num);i++){
if(a[i]>=A&&a[i]<=B)
ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
}
for(int i=(f[r]-1)*num+1;i<=r;i++){
if(a[i]>=A&&a[i]<=B)
ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
}
while(top){
if(val[stac[top]])val[stac[top]]=0;
--top;
}
printf("%d\n",ans);
}
return 0;
}
莫队
给定一段序列疯狂求区间和。
幼年不会线段树时,我想到不同区间的答案是可以线性转移的,且如果让区间尽可能的近,那么转移就尽可能的快,当时就想到将查询序列离线排序处理答案。
这就是莫队的主要思路,非常简单,不过我当时不会分块划分,所以复杂度是假的。
事实上更快的莫队需要更小的总跳跃次数,严格上讲要对查询序列跑曼哈顿距离最小生成树。不过可以用分块搞到
证明:排序后左端点按块递增,左端点块相同时右端点递增,故单个块的右端点最多跳
总复杂度
这也揭示了莫队的使用范畴
- 必须可以离线。
- 必须可以快速(
)跳下标转移答案。 - 不得有复杂修改。
这个题如果用分块会导致内存爆炸。不过没要求在线,考虑莫队。
设当前元素
减少一个时的贡献:
然后就没了。
#include<bits/stdc++.h>
#define MAXN 50005
#define int long long
using namespace std;
int n,m,k;
int a[MAXN],f[MAXN];
int num,tot;
struct node{
int l,r,id;
}q[MAXN];
inline bool cmp(node x,node y){
if(f[x.l]==f[y.l])return x.r<y.r;
return f[x.l]<f[y.l];
}
int ll=1,rr,res;
int ans[MAXN],val[MAXN];
inline void Add(int x){
res+=val[a[x]]*2+1;
++val[a[x]];
}
inline void Del(int x){
res-=(val[a[x]]-1)*2+1;
--val[a[x]];
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&k);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=m;i++){
scanf("%lld%lld",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
for(int i=1,l,r;i<=m;i++){
l=q[i].l,r=q[i].r;
while(l<ll)--ll,Add(ll);
while(l>ll)Del(ll),++ll;
while(r<rr)Del(rr),--rr;
while(r>rr)++rr,Add(rr);
ans[q[i].id]=res;
}
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
在长为
增减一个元素
然后就没了。
莫队的扩展性比较强。
本题中,开两棵 BIT 维护值域
时间复杂度
不过在根号算法中再使用
考虑优化,我们发现如果在维护值域的过程中引入分块,即将值域分块,维护块内外的答案。那么单次块上的查询是
不过只需在修改后查询,也就是说莫队操作的复杂度与查询的复杂度是独立的,总复杂度
不考虑修改的话这道题算莫队板题。不过现在有修改了。
这个东西叫做带修莫队,即支持简单的修改操作。
莫队对每个询问区间
具体地,在这道题中,在刚才的基础上只有位于查询区间的修改才能对该区间的答案造成影响。
所以参考
#include<bits/stdc++.h>
#define MAXN 1500000
#define int long long
using namespace std;
int n,m;
int num,tot;
int f[MAXN],a[MAXN];
struct node{
int l,r,t,id;
}qa[MAXN],qm[MAXN];
int va,vm;
inline bool cmp(node x,node y){
return f[x.l]==f[y.l]?(f[x.r]==f[y.r]?x.t<y.t:x.r<y.r):(x.l<y.l);
}
int ll=1,rr,tt,res;
int val[MAXN];
inline void Add(int x){
if(!val[a[x]])++res;
++val[a[x]];
}
inline void Del(int x){
--val[a[x]];
if(!val[a[x]])--res;
}
inline void Upd(int x,int tim){
if(qa[x].l<=qm[tim].l&&qm[tim].l<=qa[x].r){
Del(qm[tim].l);
int v=qm[tim].r;
if(!val[v])++res;
++val[v];
}
swap(a[qm[tim].l],qm[tim].r);
}
int ans[MAXN];
signed main(){
scanf("%lld%lld",&n,&m);
while(n*n>num*num*num)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1,l,r;i<=m;i++){
char opt[5];
scanf("%s%lld%lld",opt+1,&l,&r);
if(opt[1]=='Q'){
++va;
qa[va].l=l,qa[va].r=r,qa[va].t=vm;
qa[va].id=va;
}
else{
qm[++vm].l=l,qm[vm].r=r;
}
}
sort(qa+1,qa+1+va,cmp);
for(int i=1,l,r,t;i<=va;i++){
l=qa[i].l,r=qa[i].r,t=qa[i].t;
while(l>ll)Del(ll++);
while(l<ll)Add(--ll);
while(r>rr)Add(++rr);
while(r<rr)Del(rr--);
while(t<tt)Upd(i,tt--);
while(t>tt)Upd(i,++tt);
ans[qa[i].id]=res;
}
for(int i=1;i<=va;i++)printf("%lld\n",ans[i]);
return 0;
}
更高级的技巧会在足够强后续写。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律