あいさか たいがblogAisaka_Taiga的博客
//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

莫队算法

Toretto·2023-01-18 09:09·73 次阅读

莫队算法

莫队

不是提莫队长。

普通莫队#

莫队算法是由莫涛发明的算法,所以称为莫队算法。
莫队算法可以说是把暴力和分块融合在一起的一种算法,主要可以解决一些不强制在线的操作;
主要的思想就是通过挪动区间指针来减少时间复杂度,这个需要把每一次询问的区间给存起来,然后按照“如果区间的左端点在一个块里就按右端点从小到大排序,如果不在同一块就按左端点的大小排序”的规则,将询问排序,然后进行区间的挪动可以大大减少时间复杂度,然后把每一次得到的答案给离线存放到数组里,最后一起输出即可。

比如这道例题

P2709 小B的询问#

首先我们拿到题面,可以看到询问操作是 lr 之间的所有数的出现次数的平方的和,不难发现如果要是删掉一个数的话他的之就会减少 n2(n1)2 也就是 2n1n 是当前数在此区间内的出现次数,如果要是加入某一个数的话,那么增加的就是 (n+1)2n2 也就是 2n+1,然后就是普通莫队啦。

Copy
#include<bits/stdc++.h> #define int long long #define endl '\n' #define N 100010 using namespace std; int ans[N],a[N],b[N],n,m,k,c,kc; struct modui{int l,r,id;}e[N]; inline int read(){int x=0,fh=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-') fh=-1;ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*fh;} inline int cmp(modui a,modui b) { if((a.l-1)/kc==(b.l-1)/kc)return a.r<b.r; return a.l<b.l; } inline void add(int x) { c+=2*b[x]+1; b[x]++; } inline void dele(int x) { c-=2*b[x]-1; b[x]--; } signed main() { int L,R,ans1=1,ans2=0; n=read(),m=read(),k=read(); kc=sqrt(n); for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<=m;i++) { e[i].l=read(); e[i].r=read(); e[i].id=i; } sort(e+1,e+m+1,cmp); for(int i=1;i<=m;i++) { L=e[i].l,R=e[i].r; while(ans1>L)ans1--,add(a[ans1]); while(ans2<R)ans2++,add(a[ans2]); while(ans1<L)dele(a[ans1]),ans1++; while(ans2>R)dele(a[ans2]),ans2--; ans[e[i].id]=c; } for(int i=1;i<=m;i++) cout<<ans[i]<<endl; return 0; }

P1494 [国家集训队] 小 Z 的袜子#

题目询问对于一个区间内取两个数相同的概率,那么我们可以知道,这种取数是属于那种取出不放回的,所以我们的分母也就是取出的数的情况总数就是 (rl+1)(rl)2 ,那么我们就可以再去算取出的两个数可能是 x 的情况数,此时我们设 nx 在此区间内出现的次数,那么我们也很容易就得出 x 对于此次的询问的贡献就是 n×(n1)2,删除掉一个 x 就相当于在分子上减去 n×(n1)2(n1)(n2)2,化简完得 n1,加上一个 x 就相当于在分子上加 (n+1)×n2n×(n1)2,化简完得 n

Copy
#include<bits/stdc++.h> #define int long long #define N 100100 using namespace std; int n,m,kc,a[N],ans[N][2],b[N],fz; struct sb{int l,r,id;}e[N]; inline int read(){int x=0,fh=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-') fh=-1;ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*fh;} inline int cmp(sb a,sb b) { if((a.l-1)/kc==(b.l-1)/kc)return a.r<b.r; else return a.l<b.l; } inline void dele(int x) { fz-=b[x]-1; b[x]--; } inline void add(int x) { fz+=b[x]; b[x]++; } signed main() { int L,R,ans1=1,ans2=0; n=read();m=read(); kc=sqrt(n); for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<=m;i++) { e[i].l=read(); e[i].r=read(); e[i].id=i; } sort(e+1,e+m+1,cmp); for(int i=1;i<=m;i++) { L=e[i].l;R=e[i].r; if(L==R) { ans[e[i].id][0]=0; ans[e[i].id][1]=1; continue; } while(ans1>L)ans1--,add(a[ans1]); while(ans2<R)ans2++,add(a[ans2]); while(ans1<L)dele(a[ans1]),ans1++; while(ans2>R)dele(a[ans2]),ans2--; int c=R-L+1,fm=c*(c-1)/2; int g=__gcd(fz,fm); ans[e[i].id][0]=fz/g; ans[e[i].id][1]=fm/g; } for(int i=1;i<=m;i++) cout<<ans[i][0]<<"/"<<ans[i][1]<<endl; return 0; }

带修莫队#

首先我们要知道,普通的莫队算法是不资瓷修改操作的,不过后人对莫队算法加以改进发明了资瓷修改的莫队算法。

在进行修改操作的时候,修改操作是会对答案产生影响的(废话),那么我们如何避免修改操作带来的影响呢?首先我们需要把查询操作和修改操作分别记录下来。在记录查询操作的时候,需要增加一个变量来记录离本次查询最近的修改的位置,然后套上莫队的板子,与普通莫队不一样的是,你需要用一个变量记录当前已经进行了几次修改。对于查询操作,如果当前改的比本次查询需要改的少,就改过去,反之如果改多了就改回来。

比如,我们现在已经进行了3次修改,本次查询是在第5次修改之后,那我们就执行第4,5次修改,这样就可以避免修改操作对答案产生的影响了。

同时我们需要对排序的规则进行一下修改:如果左端点在同一区块且右端点在同一区块,则按时间排序;如果左端点在同一区块而右端点不在同一区块,则按右端点排序;如果左端点不在同一区块,则按左端点排序。

P1903 [国家集训队] 数颜色 / 维护队列#

Copy
#include<bits/stdc++.h> #define int long long #define endl '\n' #define N 1001000 using namespace std; struct node{int l,r,t,id;}e1[N]; struct Node{int id,k;}e2[N]; int n,m,kc,now,a[N],cnt[N],b[N],ans[N],cnt1,cnt2; inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;} inline int cmp(node a,node b) { if(a.l/kc==b.l/kc) { if(a.r/kc==b.r/kc)return a.t<b.t; else return a.r<b.r; } else return a.l<b.l; } inline void cxk(int l,int r,int x) { int xx=e2[x].id; int &kk=e2[x].k; if(xx>=l&&xx<=r) { now-=! --cnt[a[xx]]; now+=! cnt[kk]++; } swap(a[xx],kk); } signed main() { n=read(); m=read(); kc=pow(n,0.666); for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<=m;i++) { char op; int l,r; cin>>op; if(op=='Q') { e1[++cnt1].l=read(); e1[cnt1].r=read(); e1[cnt1].t=cnt2; e1[cnt1].id=cnt1; } else { e2[++cnt2].id=read(); e2[cnt2].k=read(); } } sort(e1+1,e1+cnt1+1,cmp); int L,R,T,l=1,r=0,t=0;now=0; for(int i=1;i<=n;i++) { L=e1[i].l; R=e1[i].r; T=e1[i].t; while(l<L)now-=! --cnt[a[l++]]; while(l>L)now+=! cnt[a[--l]]++; while(r<R)now+=! cnt[a[++r]]++; while(r>R)now-=! --cnt[a[r--]]; while(t<T)cxk(L,R,++t); while(t>T)cxk(L,R,t--); ans[e1[i].id]=now; } for(int i=1;i<=cnt1;i++) cout<<ans[i]<<endl; return 0; }

回滚莫队#

回滚莫队这个东西,一般是在加入的操作很好搞,但删除的时候很难搞的时候用的,比如问你区间内的最值问题。

当你在处理询问的时候,我们都知道当左端点处于同一块的时候,右端点是从小到大单调递增的,所以我们想到,可以先把左端点设为当前块右端点+1,然后右端点设为当前块右端点,这样只要你想查询,就必须向外扩展然后进行加的操作,我们知道右端点单调递增了,所以我们可以开一个变量存上一次的答案,然后下一次直接调用,每一次处理完一个询问就恢复左端点,然后恢复答案的值,然后下一次开始加。

对于lr在同一区间内的情况,直接暴力求答案。

板子

Copy
#include<bits/stdc++.h> #define int long long #define N 1000100 using namespace std; int n,m,kc,bl[N],ans[N],Ais,last,cnt[N],cnt1[N]; int len,a[N],v[N],cao[N],block; struct sb{int l,r,id;}e[N]; inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;} inline int cmp(sb a,sb b){if(bl[a.l]==bl[b.l])return a.r<b.r;return a.l<b.l;} inline void add(int x){++cnt[v[x]];Ais=max(Ais,cnt[v[x]]*a[x]);}//加贡献的时候计算最大值 inline void del(int x){--cnt[v[x]];}//减操作 inline int slove(int l,int r) { int maxn=0; for(int i=l;i<=r;i++)cnt1[v[i]]=0;//清空cnt1数组 for(int i=l;i<=r;i++)//枚举每一个区间找最大值 { ++cnt1[v[i]]; maxn=max(maxn,cnt1[v[i]]*a[i]); } return maxn;//返回答案 } signed main() { n=read();m=read(); kc=sqrt(n); for(int i=1;i<=n;i++) a[i]=read(),cao[i]=a[i],bl[i]=(i-1)/kc+1;//计算块,存a数组 block=bl[n];//块的数量 sort(cao+1,cao+n+1);//将cao从小到大排序 len=unique(cao+1,cao+n+1)-cao-1;//去重取出长度 for(int i=1;i<=n;i++) v[i]=lower_bound(cao+1,cao+len+1,a[i])-cao;//计算当前ai在cao中去重后的位置 for(int i=1;i<=m;i++)//输入询问的信息 e[i].l=read(),e[i].r=read(),e[i].id=i; sort(e+1,e+m+1,cmp);//将询问排序 int p=1;//当前询问的编号 for(int i=1;i<=block;i++)//枚举每一个块 { Ais=0;last=0;//ans和last清空 for(int j=1;j<=n;j++)cnt[j]=0;//清空cnt数组 int t=min(kc*i,n);//极限边界 int l=t+1,r=t;//左边界一开始最大,r一开始等于当前块右端点 for(;bl[e[p].l]==i;p++)//如果当前询问的左端点是在当前块里就询问的编号不断累加 { if(bl[e[p].l]==bl[e[p].r])//左右端点在同一块里 { ans[e[p].id]=slove(e[p].l,e[p].r);//直接暴力求值 continue;//跳过 } while(r<e[p].r)add(++r);//如果要是右边界小就加 last=Ais;//last记录当前的答案,l为右端点的答案 while(l>e[p].l)add(--l);//如果要是当前点的左端点的大于询问的左端点,直接加 ans[e[p].id]=Ais;//得到答案 while(l<=t)del(l++);//恢复左端点 Ais=last;//撤回答案 } } for(int i=1;i<=m;i++) cout<<ans[i]<<endl;//输出答案 return 0; }
posted @   北烛青澜  阅读(73)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示
目录