莫队算法——从入门到黑题 by WAMonster
(感谢yspm的推荐
板子
对位置进行分块,记录询问每个端点所属的块,排序时,左端点按所属的块排序,右端点按原位置排序
淦发现之前在博客里发出了“莫队左端点移动O(n)”的暴论
排序后,左端点移动的次数是(O(nsqrt{n}))的,
而右端点在每个相同左端点中递增,移动的次数是(O(nsqrt{n})),所以算法总复杂度为(O(nsqrt{n}))
还有一些奇怪的优化比如奇偶性排序,但是并不会证只能先背过了……
SP3267 DQUERY - D-query
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 30005
#define Q 200005
#define C 1000005
ll n,a[N],bel[N];
ll q;
struct query
{
ll a,b,id;
ll aa,bb;
bool operator<(const query &y)const
{
return aa==y.aa?b<y.b:aa<y.aa;
}
}qq[Q];
inline ll read()
{
ll f=0,s=0; char c=getchar();
while(c>'9'||c<'0')f=(c=='-'),c=getchar();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
return f?-s:s;
}
ll cnt[C];
ll an=0;
ll ans[Q];
void add(ll x)
{
if(!cnt[a[x]])an++;
cnt[a[x]]++;
}
void del(ll x)
{
cnt[a[x]]--;
if(!cnt[a[x]])an--;
}
int main()
{
n=read();
for(int i=1;i<=n;i++)a[i]=read();
ll S=sqrt(n)+1;
for(int i=1;i<=n;i++)bel[i]=(i-1)/S+1;
q=read();
for(int i=1;i<=q;i++)
{
qq[i].a=read(),qq[i].b=read(),qq[i].id=i;
qq[i].aa=bel[qq[i].a],qq[i].bb=bel[qq[i].b];
}
sort(qq+1,qq+q+1);
ll l=1,r=0;
for(int i=1;i<=q;i++)
{
ll nl=qq[i].a,nr=qq[i].b;
while(nl>l)del(l++);
while(nl<l)add(--l);
while(nr>r)add(++r);
while(nr<r)del(r--);
ans[qq[i].id]=an;
}
for(int i=1;i<=q;i++)printf("%lld
",ans[i]);
return 0;
}
luogu P2709 小B的询问
一个挺有用的统计平方和的套路:从(a^2)到((a+1)^2),即为加上(2a+1)
所以只需要统计每种颜色当前区间内的出现次数就好
二维莫队
蔬菜
原理和一维差不多,只不过是相当于区间变成了矩形,所以移动的时候要对一个区间进行加加减减
用二维莫队就可以转化成和小B的询问套路相同的题
带修莫队
P1903 [国家集训队]数颜色 / 维护队列
与普通莫队的区别在于支持修改,当然莫队需要离线
若强制在线就得考虑别的方法了,但是如果支持修改且可以离线有时就可以考虑带修莫队
为了支持修改,带修莫队需要在普通莫队的基础上加一个时间戳,时间戳记录的是每个询问上一个修改操作的时间,
把修改操作单独储存后,就可以通过时间戳的加减来实现对于某个区间答案的时间变换
感性理解:就是由原来的((l+1,r)),((l-1,r)),((l,r+1)),((l,r-1))四个方向的转移
变成了((l+1,r,t)),((l-1,r,t)),((l,r+1,t)),((l,r-1,t)),((l,r,t+1)),((l,r,t-1))六个方向的转移
转移的时候需要记录当前值以备转移回去,可以直接把当前值与修改操作中的修改值交换,这样回来的时候就能直接换回去
奇偶性排序直接12s->5s是什么操作
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 133350
#define C 1000005
ll n,m;
ll a[N];
ll cnt[C],ans[N];
struct query
{
ll l,r,t,id;
ll lb,rb;
bool operator<(const query &y)const
{
return (lb^y.lb)?lb<y.lb:(rb^y.rb)?rb<y.rb:t<y.t;
}
}q[N];
ll qcn=0;
struct update
{
ll x,v;
}c[N];
inline ll read()
{
ll f=0,s=0; char c=getchar();
while(c>'9'||c<'0')f=(c=='-'),c=getchar();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
return f?-s:s;
}
ll ti=0,an=0;
char ch[2];
void add(ll x=0,ll c=0)
{
if(x)
{
if(cnt[a[x]]==0)an++;
cnt[a[x]]++;
}
else if(c)
{
if(cnt[c]==0)an++;
cnt[c]++;
}
}
void del(ll x=0,ll c=0)
{
if(x)
{
cnt[a[x]]--;
if(cnt[a[x]]==0)an--;
}
else if(c)
{
cnt[c]--;
if(cnt[c]==0)an--;
}
}
int main()
{
n=read(); m=read();
ll S=pow(n,3.0/4.0);
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++)
{
scanf("%s",ch);
if(ch[0]=='Q')
{
q[++qcn].l=read(),q[qcn].r=read(),q[qcn].t=ti,q[qcn].id=qcn;
q[qcn].lb=(q[qcn].l-1)/S+1,q[qcn].rb=(q[qcn].r-1)/S+1;
}
else if(ch[0]=='R')
{
c[++ti].x=read(),c[ti].v=read();
}
}
sort(q+1,q+qcn+1);
ll l=1,r=0,t=0;
for(int i=1;i<=qcn;i++)
{
ll nl=q[i].l,nr=q[i].r,lt=q[i].t;
while(l<nl)del(l++,0);
while(l>nl)add(--l,0);
while(r<nr)add(++r,0);
while(r>nr)del(r--,0);
while(t>lt)
{
if(nl<=c[t].x&&c[t].x<=nr)del(c[t].x,0),add(0,c[t].v);
swap(a[c[t].x],c[t].v);
t--;
}
while(t<lt)
{
t++;
if(nl<=c[t].x&&c[t].x<=nr)del(c[t].x,0),add(0,c[t].v);
swap(a[c[t].x],c[t].v);
}
ans[q[i].id]=an;
}
for(int i=1;i<=qcn;i++)printf("%lld
",ans[i]);
return 0;
}
树上莫队
SP10707 COT2
考虑怎样把树上的点转化成一个容易用莫队处理的序列,那么可以使用欧拉序
考虑路径((u,v)(dep_u<dep_v))上的点在欧拉序里是怎么表现的
首先,忽略在一段欧拉序中出现两次的点,这些点不在所求路径上
记点(k)在欧拉序中第一次出现在(fi_k),最后一次出现在(la_k),那么:
- 如果(lca(u,v)=u),那么所求区间为([fi_u,fi_v])
- 否则,所求区间为([la_u,fi_v]),但这个区间不包含他们的lca,这时lca要单独处理
这样,就可以用莫队处理树上的问题了
code:
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull unsigned long long
#define N 200005
#define gc() (p1==p2?(p2=buf+fread(p1=buf,1,1<<20,stdin),p1==p2?EOF:*p1++):*p1++)
char buf[1<<20],*p1,*p2;
ll n,m;
ll w[N],b[N],tot=0;
ll t1,t2;
ll v[N];
ll h[N],nx[N],tt=0;
void add(ll a,ll b){
v[++tt]=b;
nx[tt]=h[a];
h[a]=tt;
}
ll bel[N];
struct query{
ll l,r,id,lc;
ll lb,rb;
bool operator<(const query &b)const{
return (bel[l]^bel[b.l])?(bel[l]<bel[b.l]):((bel[l]&1)?r<b.r:r>b.r);
}
}q[N];
ll a[N*2],fi[N],la[N],cnt=0;
ll fa[N][21],dep[N];
void dfs(ll u,ll f){
a[++cnt]=u;
fa[u][0]=f;
dep[u]=dep[f]+1;
fi[u]=cnt;
for(int i=1;i<=20;i++){
fa[u][i]=fa[fa[u][i-1]][i-1];
}
for(int i=h[u];i;i=nx[i]){
if(v[i]==f)continue;
dfs(v[i],u);
}
a[++cnt]=u;
la[u]=cnt;
}
inline int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;--i)if(dep[u]-(1<<i)>=dep[v])u=fa[u][i];
if(u==v)return u;
for(int i=20;i>=0;--i)if(fa[u][i]!=fa[v][i])u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
bool vis[N];
ll cn[N];
ll an=0,ans[N];
inline void work(int pos){
vis[pos]?an-=!--cn[w[pos]]:an+=!cn[w[pos]]++;
vis[pos]^=1;
}
inline ll read(){
ll f=0,s=0; char c=gc();
while(c>'9'||c<'0')f=(c=='-'),c=gc();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=gc();
return f?-s:s;
}
int main(){
n=read(), m=read();
for(int i=1;i<=n;i++)w[i]=read(),b[++tot]=w[i];
sort(b+1,b+tot+1); tot=unique(b+1,b+tot+1)-b-1;
for(int i=1;i<=n;i++)w[i]=lower_bound(b+1,b+tot+1,w[i])-b;//cout<<w[i]<<' ';cout<<endl;
for(int i=1;i<n;i++)t1=read(),t2=read(),add(t1,t2),add(t2,t1);
dfs(1,0);
ll S=sqrt(cnt),bnum=ceil((double)cnt/S);
for(int i=1;i<=bnum;++i)for(int j=S*(i-1)+1;j<=i*S;++j){
bel[j]=i;
}
for(int i=1;i<=m;i++){
q[i].l=read(),q[i].r=read(),q[i].id=i;
ll tlc=lca(q[i].l,q[i].r);
if(fi[q[i].l]>fi[q[i].r])swap(q[i].l,q[i].r);
if(tlc==q[i].l)q[i].l=fi[q[i].l],q[i].r=fi[q[i].r];
else q[i].l=la[q[i].l],q[i].r=fi[q[i].r],q[i].lc=tlc;
}
sort(q+1,q+m+1);
ll l=1,r=0;
for(int i=1;i<=m;i++){
ll nl=q[i].l,nr=q[i].r;
while(l<nl)work(a[l++]);
while(l>nl)work(a[--l]);
while(r<nr)work(a[++r]);
while(r>nr)work(a[r--]);
if(q[i].lc)work(q[i].lc);
ans[q[i].id]=an;
if(q[i].lc)work(q[i].lc);
}
for(int i=1;i<=m;i++)printf("%d
",ans[i]);
return 0;
}
需要注意的:
- 莫队中的分块,在点的编号较小,询问较多时,先统一求出每个数所属的块,不用每个询问单独算
- lca倍增预处理(lg)数组会变慢,最好直接根据数据范围定下上界(21,31这种的)
要是这两点出了岔子程序效率就会极其低……
回滚莫队
有时候题目中并不是很好实现莫队移动区间时的“删除”操作,这时可以用回滚莫队来绕开删除的不便
具体的实现是把询问按左端点所属的块排序,块内按右端点编号排序,左右端点在同一块就暴力算,否则右端点从当前块右端出发向右走(这个移动排序后是单调的),左端点每次从当前块右端向左走,处理完每个询问后再把左端点移到块右端,并把答案恢复到上一次左端点走之前的状态
具体细节见代码
洛谷 P5906 【模板】回滚莫队&不删除莫队
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 200005
ll n,m,a[N],b[N],S,tot=0;
ll bel[N];
struct query{
ll l,r,id;
inline bool operator<(const query &b)const{
return (bel[l]==bel[b.l])?(r<b.r):(bel[l]<bel[b.l]);
}
}q[N];
ll pre[N],lst[N],ans[N];
ll st[N],tp=0;
inline ll work(ll l,ll r){ ll an=0;
for(int i=l;i<=r;i++){
if(!pre[a[i]])pre[a[i]]=i,st[++tp]=a[i];
an=max(an,i-pre[a[i]]);
}
while(tp)pre[st[tp]]=0,tp--;
return an;
}
inline ll read(){
ll f=0,s=0; char c=getchar();
while(c>'9'||c<'0')f=(c=='-'),c=getchar();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
return f?-s:s;
}
int main(){
n=read(); for(int i=1;i<=n;i++)a[i]=b[++tot]=read();
m=read(); for(int i=1;i<=m;i++)q[i].l=read(), q[i].r=read(), q[i].id=i;
S=sqrt(n)+1; for(int i=1;i<=n;i++)bel[i]=(i-1)/S+1;
sort(b+1,b+tot+1); tot=unique(b+1,b+tot+1)-b-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+tot+1,a[i])-b;
sort(q+1,q+m+1);
for(int i=1,j=1;i<=S;i++){ ll br=i*S, l=br+1, r=br, an=0;
for(;bel[q[j].l]==i;j++){ ll ql=q[j].l, qr=q[j].r;
if(bel[ql]==bel[qr]){ ans[q[j].id]=work(ql,qr); continue; }
while(r<qr){
r++; lst[a[r]]=r;
if(!pre[a[r]])pre[a[r]]=r,st[++tp]=a[r]; an=max(an,r-pre[a[r]]);
}
ll tmp=an;
while(l>ql){
l--; if(!lst[a[l]])lst[a[l]]=l,st[++tp]=a[l];
an=max(an,lst[a[l]]-l);
}
ans[q[j].id]=an;
while(l<=br){
if(lst[a[l]]==l)lst[a[l]]=0; l++;
}
an=tmp;
}
while(tp)pre[st[tp]]=lst[st[tp]]=0,tp--;
}
for(int i=1;i<=m;i++)printf("%lld
",ans[i]);
return 0;
}