莫队学习笔记
常规莫队
莫队用来处理一系列离线问题,可以在较短时间内移动一步区间的操作可以使用莫队来解决。
时间复杂度
传统的莫队只需要解决区间上的问题,因此只包含左端点和右端点,一般是按照 \(\sqrt n\) 的块长,以 \(l\) 所在的块为第一关键字,以 \(r\) 为第二关键字。
这样在 \(O(1)\) 的单次修改下,时间复杂度为 \(O(n\sqrt n)\)(\(n\) 与 \(q\) 同级)。
后面还会出现多个变种,只需要知道在最劣情况下,左端点会走 \(q\) 次块长,右端点会走块数组合数次的全局即可得到最优块长并且得到时间复杂度。
以常规莫队为例,设块长为 \(B\) 那么左端为 \(qB\) 右端为 \(\frac {n^2}B\)
在最佳情况下左右两端相等 \(B=\sqrt{\frac {n^2}q}\)
例题
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,B,a[50005],cnt[50005],sum,ans[50005],ans2[50005];
struct Q{
int l,r,id;
}b[50005];
bool cmp(Q a,Q b){
if((a.l-1)/B==(b.l-1)/B){
if((a.l-1)/B&1)return a.r>b.r;
else return a.r<b.r;
}
return a.l<b.l;
}
void add(int x){
sum-=cnt[a[x]]*(cnt[a[x]]-1);
cnt[a[x]]++;
sum+=cnt[a[x]]*(cnt[a[x]]-1);
}
void del(int x){
sum-=cnt[a[x]]*(cnt[a[x]]-1);
cnt[a[x]]--;
sum+=cnt[a[x]]*(cnt[a[x]]-1);
}
signed main(){
cin>>n>>m;
B=sqrt(n)+1;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++)cin>>b[i].l>>b[i].r,b[i].id=i;
sort(b+1,b+m+1,cmp);
cnt[a[1]]=1;
int l=1,r=1;
for(int i=1;i<=m;i++){
while(r<b[i].r)add(++r);
while(l>b[i].l)add(--l);
while(r>b[i].r)del(r--);
while(l<b[i].l)del(l++);
ans[b[i].id]=sum;
ans2[b[i].id]=(b[i].r-b[i].l+1)*(b[i].r-b[i].l);
}
for(int i=1;i<=m;i++){
if(ans2[i]==0)puts("0/1");
else {
int tmp=__gcd(ans[i],ans2[i]);
ans[i]/=tmp,ans2[i]/=tmp;
cout<<ans[i]<<'/'<<ans2[i]<<endl;
}
}
return 0;
}
带修改莫队
因为时间的影响我们也可以处理,所以加入一维时间轴即可。
时间复杂度
因为时间的影响我们也可以处理,所以加入一维时间轴即可。
那么对于这个莫队,左端点仍然是 \(qB\) 次操作,然而右端点是 \(\frac {n^3}{B^2}\)
那么 \(B=\sqrt[3]{\frac {n^3}{q}}\)
若 \(n,q\) 同级,那么就是 \(O(n^{\frac{5}{3}})\)
注意在使用时由于还会1返回,所以需要将修改的内容与修改前内容交换,这样才能够正确返回
例题
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[150005],B,cnt[1000005],sum,ans[150005],cntb,cntc;
char op;
struct Q{
int l,r,t,id;
}b[150005];
struct P{
int p,v;
}c[150005];
bool cmp(Q a,Q b){
if((a.l-1)/B!=(b.l-1)/B)return a.l<b.l;
if((a.r-1)/B!=(b.r-1)/B)return a.r<b.r;
return a.t<b.t;
}
void add(int x){
if(!cnt[x])sum++;
cnt[x]++;
}
void del(int x){
cnt[x]--;
if(!cnt[x])sum--;
}
signed main(){
cin>>n>>m;
B=pow(n,0.666);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1,l,r;i<=m;i++){
cin>>op>>l>>r;
if(op=='Q')cntb++,b[cntb]={l,r,cntc,cntb};
else c[++cntc]={l,r};
}
sort(b+1,b+cntb+1,cmp);
int l=1,r=1,t=0;
add(a[1]);
for(int i=1;i<=cntb;i++){
while(b[i].l<l)add(a[--l]);
while(b[i].r>r)add(a[++r]);
while(b[i].l>l)del(a[l++]);
while(b[i].r<r)del(a[r--]);
while(t<b[i].t){
t++;
if(c[t].p<=r&&c[t].p>=l){
add(c[t].v);
del(a[c[t].p]);
}
swap(c[t].v,a[c[t].p]);
}
while(t>b[i].t){
if(c[t].p<=r&&c[t].p>=l){
add(c[t].v);
del(a[c[t].p]);
}
swap(c[t].v,a[c[t].p]);
t--;
}
ans[b[i].id]=sum;
}
for(int i=1;i<=cntb;i++)cout<<ans[i]<<endl;
return 0;
}
回滚莫队
当删除操作不好完成时,我们可以使用回滚莫队来避免删除操作。
首先我们将左右端点都放置于块的最右端,先移动右端点,并记录下答案,然后移动左端点,得到答案以后让左端点返回。
时间复杂度
由于只是每次往回跑,多移动一次,时间复杂度不变,常数变大
例题
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[100005],B,cnt[100005],sum,tmp[100005],id[100005],L[405],R[405],ans[100005],c[100005];
struct P{
int l,r,id;
}b[100005];
bool cmp(P a,P b){
if(id[a.l]!=id[b.l])return a.l<b.l;
return a.r<b.r;
}
void add(int x){
cnt[x]++;
sum=max(sum,cnt[x]*tmp[x]);
}
void del(int x){
cnt[x]--;
}
signed main(){
cin>>n>>m;
B=sqrt(n)+1;
for(int i=1;i<=n;i++){
id[i]=(i-1)/B+1;
if(!L[id[i]])L[id[i]]=i;
R[id[i]]=i;
}
for(int i=1;i<=n;i++)cin>>a[i],tmp[i]=a[i];
sort(tmp+1,tmp+n+1);
int cntt=unique(tmp+1,tmp+n+1)-tmp-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(tmp+1,tmp+cntt+1,a[i])-tmp;
for(int i=1,l,r;i<=m;i++){
cin>>l>>r;
b[i]={l,r,i};
}
sort(b+1,b+m+1,cmp);
int l=1,r=0,la=0;
for(int i=1;i<=m;i++){
if(id[b[i].l]==id[b[i].r]){
sum=0;
for(int j=b[i].l;j<=b[i].r;j++){
c[a[j]]++;
sum=max(sum,c[a[j]]*tmp[a[j]]);
}
for(int j=b[i].l;j<=b[i].r;j++)c[a[j]]--;
ans[b[i].id]=sum;
continue;
}
if(id[b[i].l]!=la){
sum=0;
la=id[b[i].l];
while(r>R[la])del(a[r--]);
while(l<R[la]+1)del(a[l++]);
}
while(r<b[i].r)add(a[++r]);
int tmp=sum;
while(l>b[i].l)add(a[--l]);
ans[b[i].id]=sum;
sum=tmp;
while(l<R[la]+1)del(a[l++]);
}
for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
return 0;
}
二维莫队
处理二维莫队问题通常需要处理左上、左下、右上、右下四个指针。
时间复杂度
首先维护四个点,矩形的长和宽同级,那么假设边长为 \(n\),计算得到左端移动 \(qB\) 右端移动 \(\frac {n^4}{B^3}\) 次,让两边相等,可得 \(B=\sqrt[4]{\frac {n^4}{q}}\) 时为最佳块长。
假设 \(n\) 和 \(q\) 同级,那么时间复杂度就是 \(O(n^{\frac {11}{4}})\) ,因为每次操作不是 \(O(1)\) 而是 \(O(n)\)
但是大部分题目并不是同级的,不然你这时间复杂度也就比我直接暴力好一点点,一般都是 \(q\) 比较大的。
那么重新计算一下,\(q\times B\times n=n^2q^{\frac{3}{4}}\) ,时间复杂度其实还是挺大的。
对比常规的方法,这样的优化效果并不明显,所以题目突破口有可能存在于能否优化 \(O(n)\) 的单次修改操作,比如二次离线等等方式。
例题
理论时间复杂度不对,但是可以莫队疯狂卡常卡过。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[501][501],tmp[250001],cntt,ans[60001],id1[501],id2[250001],cnt[501],cnt2[250001];
inline int read(){
char c=getchar();
int x=0;
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x;
}
struct Q{
int x,y,xx,yy,k,id;
}b[60005];
inline bool cmp(Q a,Q b){
if(id1[a.x]!=id1[b.x])return a.x<b.x;
if(id1[a.y]!=id1[b.y]){
if(id1[a.x]&1)return a.y<b.y;
return a.y>b.y;
}
if(id1[a.xx]!=id1[b.xx]){
if(id1[a.y]&1)return a.xx<b.xx;
return a.xx>b.xx;
}
if(id1[a.xx]&1)return a.yy<b.yy;
return a.yy>b.yy;
}
signed main(){
n=read(),m=read();
int B=n*pow(m,-0.25)/3+1;
for(register int i=1;i<=n;i++)id1[i]=(i-1)/B+1;
for(register int i=1;i<=n;i++)for(register int j=1;j<=n;j++)a[i][j]=read(),tmp[++cntt]=a[i][j];
sort(tmp+1,tmp+cntt+1);
cntt=unique(tmp+1,tmp+cntt+1)-tmp-1;
int B2=sqrt(cntt)+1;
for(register int i=1;i<=n*n;i++)id2[i]=(i-1)/B2+1;
for(register int i=1;i<=n;i++)for(register int j=1;j<=n;j++)a[i][j]=lower_bound(tmp+1,tmp+cntt+1,a[i][j])-tmp;
for(register int i=1;i<=m;i++){
b[i].x=read(),b[i].y=read(),b[i].xx=read(),b[i].yy=read(),b[i].k=read();
b[i].id=i;
}
sort(b+1,b+m+1,cmp);
int x=1,y=1,xx=1,yy=1;
cnt[id2[a[1][1]]]++;
cnt2[a[1][1]]++;
for(register int i=1;i<=m;i++){
while(x>b[i].x){
x--;
for(register int j=y;j<=yy;j++)cnt[id2[a[x][j]]]++,cnt2[a[x][j]]++;
}
while(y>b[i].y){
y--;
for(register int j=x;j<=xx;j++)cnt[id2[a[j][y]]]++,cnt2[a[j][y]]++;
}
while(xx<b[i].xx){
xx++;
for(register int j=y;j<=yy;j++)cnt[id2[a[xx][j]]]++,cnt2[a[xx][j]]++;
}
while(yy<b[i].yy){
yy++;
for(register int j=x;j<=xx;j++)cnt[id2[a[j][yy]]]++,cnt2[a[j][yy]]++;
}
while(x<b[i].x){
for(register int j=y;j<=yy;j++)cnt[id2[a[x][j]]]--,cnt2[a[x][j]]--;
x++;
}
while(y<b[i].y){
for(register int j=x;j<=xx;j++)cnt[id2[a[j][y]]]--,cnt2[a[j][y]]--;
y++;
}
while(xx>b[i].xx){
for(register int j=y;j<=yy;j++)cnt[id2[a[xx][j]]]--,cnt2[a[xx][j]]--;
xx--;
}
while(yy>b[i].yy){
for(register int j=x;j<=xx;j++)cnt[id2[a[j][yy]]]--,cnt2[a[j][yy]]--;
yy--;
}
int sum=0,w=1;
while(sum+cnt[w]<b[i].k)sum+=cnt[w++];
w=(w-1)*B2+1;
while(sum+cnt2[w]<b[i].k)sum+=cnt2[w++];
ans[b[i].id]=tmp[w];
}
for(register int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}
树上莫队
并非树上,实际上只是把树变成了dfn序再进行莫队,实际上与线性相同,需要把一段的贡献拆开再两条链上。
例题
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,q,v[100005],w[100005],c[100005],st[100005][20],dep[100005],L[100005],R[100005],pos[200005],cntt,cnt[100005],res,B,ans[100005];
vector<int> e[100005];
bool vis[100005];
struct Q{
int lca,l,r,t,id;
}b[100005];
bool cmp(Q a,Q b){
if((a.l-1)/B!=(b.l-1)/B)return a.l<b.l;
if((a.r-1)/B!=(b.r-1)/B)return a.r<b.r;
return a.t<b.t;
}
pair<int,int> a[100005];
void dfs(int p,int f){
pos[++cntt]=p;
L[p]=cntt;
dep[p]=dep[f]+1;
st[p][0]=f;
for(int i=1;i<=19;i++)st[p][i]=st[st[p][i-1]][i-1];
for(int i:e[p]){
if(i==f)continue;
dfs(i,p);
}
pos[++cntt]=p;
R[p]=cntt;
}
int LCA(int l,int r){
if(dep[l]<dep[r])swap(l,r);
for(int i=19;i>=0;i--)if(dep[st[l][i]]>=dep[r])l=st[l][i];
if(l==r)return l;
for(int i=19;i>=0;i--){
if(st[l][i]!=st[r][i]){
l=st[l][i];
r=st[r][i];
}
}
return st[l][0];
}
void add(int x){
cnt[x]++;
res+=w[cnt[x]]*v[x];
}
void del(int x){
res-=w[cnt[x]]*v[x];
cnt[x]--;
}
void solve(int x){
if(!vis[x])add(c[x]);
else del(c[x]);
vis[x]^=1;
}
signed main(){
cin>>n>>m>>q;
for(int i=1;i<=m;i++)cin>>v[i];
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
for(int i=1;i<=n;i++)cin>>c[i];
dfs(1,0);
int cnta=0,cntb=0;
for(int i=1,op,x,y;i<=q;i++){
cin>>op>>x>>y;
if(op==0)a[++cnta]={x,y};
else {
int lca=LCA(x,y);
cntb++;
if(lca!=x&&lca!=y)b[cntb]={lca,min(R[x],R[y]),max(L[x],L[y]),cnta,cntb};
else b[cntb]={0,min(L[x],L[y]),max(L[x],L[y]),cnta,cntb};
}
}
B=pow(n,0.666)+1;
sort(b+1,b+cntb+1,cmp);
int l=1,r=0,t=0;
for(int i=1;i<=cntb;i++){
while(r<b[i].r)solve(pos[++r]);
while(r>b[i].r)solve(pos[r--]);
while(l>b[i].l)solve(pos[--l]);
while(l<b[i].l)solve(pos[l++]);
while(t<b[i].t){
t++;
if((L[a[t].first]<=r&&L[a[t].first]>=l)^(R[a[t].first]<=r&&R[a[t].first]>=l))solve(a[t].first);
swap(c[a[t].first],a[t].second);
if((L[a[t].first]<=r&&L[a[t].first]>=l)^(R[a[t].first]<=r&&R[a[t].first]>=l))solve(a[t].first);
}
while(t>b[i].t){
if((L[a[t].first]<=r&&L[a[t].first]>=l)^(R[a[t].first]<=r&&R[a[t].first]>=l))solve(a[t].first);
swap(c[a[t].first],a[t].second);
if((L[a[t].first]<=r&&L[a[t].first]>=l)^(R[a[t].first]<=r&&R[a[t].first]>=l))solve(a[t].first);
t--;
}
if(b[i].lca)solve(b[i].lca);
ans[b[i].id]=res;
if(b[i].lca)solve(b[i].lca);
}
for(int i=1;i<=cntb;i++)cout<<ans[i]<<endl;
return 0;
}
二次离线莫队
有的东西你没有办法 \(O(1)\) 求出,但是你把一堆询问放在一起问,效率就会很高,那么就能把需要的量一次莫队预处理出来,然后得到结果,再去跑莫队即可。
由于查询的量比较多,所以大部分题目需要在离线后 \(O(1)\) 查询,比较常见的方法是分块预处理,直接 \(O(\sqrt n)\) 的单次修改,反正你的莫队也带根号,没有影响。
例题
P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
直接综上处理。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100005],tmp[100005],cntt,B,pre[200005],suf[200005],L[505],R[505],id[100005],tag[505],c[100005];
long long res[200005],ans[100005];
inline int read(){
char c=getchar();
int x=0;
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x;
}
struct Q{
int l,r,op;
}b[100005];
vector<Q> er[100005],el[100005];
bool cmp(Q a,Q b){
if((a.l-1)/B!=(b.l-1)/B)return a.l<b.l;
return a.r<b.r;
}
struct BT{
int c[100005];
int lowbit(int x){
return x&-x;
}
void add(int x,int y){
for(int i=x;i<=cntt;i+=lowbit(i))c[i]+=y;
}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))res+=c[i];
return res;
}
}bit;
void init(){
n=read(),m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)tmp[i]=a[i];
sort(tmp+1,tmp+n+1);
cntt=unique(tmp+1,tmp+n+1)-tmp-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(tmp+1,tmp+cntt+1,a[i])-tmp;
B=sqrt(n)+1;
for(int i=1;i<=m;i++)b[i].l=read(),b[i].r=read(),b[i].op=i;
sort(b+1,b+m+1,cmp);
for(int i=1;i<=n;i++){
pre[i]=bit.query(a[i]);
bit.add(a[i],1);
}
for(int i=1;i<=n;i++)bit.add(a[i],-1);
for(int i=n;i>=1;i--){
suf[i]=bit.query(a[i]-1);
bit.add(a[i],1);
}
}
void Mo1(){
int l=1,r=0,w=0;
for(int i=1;i<=m;i++){
if(r<b[i].r)el[l].push_back({r+1,b[i].r,++w}),r=b[i].r;
if(r>b[i].r)el[l].push_back({b[i].r+1,r,++w}),r=b[i].r;
if(l>b[i].l)er[r].push_back({b[i].l,l-1,++w}),l=b[i].l;
if(l<b[i].l)er[r].push_back({l,b[i].l-1,++w}),l=b[i].l;
}
}
void change(int l,int r,int v){
if(id[l]==id[r]){
for(int i=l;i<=r;i++)c[i]+=v;
return;
}
for(int i=l;i<=R[id[l]];i++)c[i]+=v;
for(int i=L[id[r]];i<=r;i++)c[i]+=v;
for(int i=id[l]+1;i<id[r];i++)tag[i]+=v;
}
int query(int w){
return tag[id[w]]+c[w];
}
void solve(){
B=sqrt(cntt)+1;
for(int i=1;i<=cntt;i++){
id[i]=(i-1)/B+1;
if(!L[id[i]])L[id[i]]=i;
R[id[i]]=i;
}
for(int i=1;i<=n;i++){
for(auto tmp:el[i]){
int l=tmp.l,r=tmp.r,id=tmp.op;
for(int j=l;j<=r;j++)res[id]+=j-1-pre[j]-query(a[j]+1);
}
change(1,a[i],1);
}
memset(c,0,sizeof(c)),memset(tag,0,sizeof(tag));
for(int i=n;i>=1;i--){
for(auto tmp:er[i]){
int l=tmp.l,r=tmp.r,id=tmp.op;
for(int j=l;j<=r;j++)res[id]+=suf[j]-query(a[j]-1);
}
change(a[i],cntt,1);
}
}
void Mo2(){
int l=1,r=0,w=0;
long long sum=0;
for(int i=1;i<=m;i++){
if(r<b[i].r)sum+=res[++w],r=b[i].r;
if(r>b[i].r)sum-=res[++w],r=b[i].r;
if(l>b[i].l)sum+=res[++w],l=b[i].l;
if(l<b[i].l)sum-=res[++w],l=b[i].l;
ans[b[i].op]=sum;
}
}
signed main(){
init();
Mo1();
solve();
Mo2();
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}

浙公网安备 33010602011771号