莫队算法
莫队
不是提莫队长。
普通莫队
莫队算法是由莫涛发明的算法,所以称为莫队算法。
莫队算法可以说是把暴力和分块融合在一起的一种算法,主要可以解决一些不强制在线的操作;
主要的思想就是通过挪动区间指针来减少时间复杂度,这个需要把每一次询问的区间给存起来,然后按照“如果区间的左端点在一个块里就按右端点从小到大排序,如果不在同一块就按左端点的大小排序”的规则,将询问排序,然后进行区间的挪动可以大大减少时间复杂度,然后把每一次得到的答案给离线存放到数组里,最后一起输出即可。
比如这道例题
P2709 小B的询问
首先我们拿到题面,可以看到询问操作是 \(l\),\(r\) 之间的所有数的出现次数的平方的和,不难发现如果要是删掉一个数的话他的之就会减少 \(n^{2}-(n-1)^{2}\) 也就是 \(2n-1\),\(n\) 是当前数在此区间内的出现次数,如果要是加入某一个数的话,那么增加的就是 \((n+1)^{2}-n^{2}\) 也就是 \(2n+1\),然后就是普通莫队啦。
#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 的袜子
题目询问对于一个区间内取两个数相同的概率,那么我们可以知道,这种取数是属于那种取出不放回的,所以我们的分母也就是取出的数的情况总数就是 \(\frac{(r-l+1)(r-l)}{2}\) ,那么我们就可以再去算取出的两个数可能是 \(x\) 的情况数,此时我们设 \(n\) 为\(x\) 在此区间内出现的次数,那么我们也很容易就得出 \(x\) 对于此次的询问的贡献就是 \(\frac{n\times (n-1)}{2}\),删除掉一个 \(x\) 就相当于在分子上减去 \(\frac{n\times (n-1)}{2}-\frac{(n-1)(n-2)}{2}\),化简完得 \(n-1\),加上一个 \(x\) 就相当于在分子上加 \(\frac{(n+1)\times n}{2}-\frac{n\times (n-1)}{2}\),化简完得 \(n\)。
#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 [国家集训队] 数颜色 / 维护队列
#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在同一区间内的情况,直接暴力求答案。
#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;
}
本文来自博客园,作者:北烛青澜,转载请注明原文链接:https://www.cnblogs.com/Multitree/p/17059135.html
The heart is higher than the sky, and life is thinner than paper.