莫队

更新日志 2025/05/24:开工。

2025/08/14:添加莫二离。


简介

(普通)莫队算法是一种离线算法,可以在 \(O(n\sqrt n)\) 的复杂度里解决一些多次区间查询问题。

其他莫队算法本质上与普通莫队算法本质相同或相似。

基础

我们考虑动态维护一个区间,包括其左端点、右端点以及这个区间的答案。

处理查询操作时,我们将左端点、右端点分别逐位移动到当前查询的区间上,并逐位更新答案。

这就是莫队的基础思想。

普通莫队

实现

我们考虑将原序列分块,通常采取 \(\frac{n}{\sqrt m}\) 块长。

然后我们对所有查询进行排序,因此莫队是一个离线算法:

  1. 左端点位于同一块的查询,若这是第奇数个块,按右端点升序排序;否则,按右端点降序排序。
  2. 左端点不位于同一块的查询,按左端点升序排序。

然后执行上面的基础算法即可。

复杂度

首先,设块长为 \(b\),莫队的复杂度是 \(O(mb+\frac{n^2}{b})\) 的。

块长 \(\frac{n}{\sqrt m}\) 可以做到 \(O(n\sqrt m)\)

我们分成三段考虑:

排序

不影响复杂度,\(O(m\log m)\)

左端点

考虑同一个块内的左端点,每次移动最多移 \(b\) 长度,共 \(m\) 次询问,这一部分的复杂度是 \(O(mb)\) 的。

对于不同块的复杂度,从一个块到达另一个块后就不会回到之前的块了,因此每个点至多只会被跨越块的移动经过两次,也就是 \(O(n)\)。更详细的,到达这个块时走到右端点,离开这个块时从左端点走起,那么块内所有点就会被跨越块的路程经过两次。

右端点

同一个块内的右端点是有序的,因此处理一个块就需要 \(O(n)\) 的复杂度,共 \(\frac{n}{b}\) 个块,共总复杂度是 \(O(\frac{n^2}{b})\) 的。

之所以按奇偶性处理升降序,你可以理解为右端点先过去再回来,如果都是升序,就多了一次从末尾回到开头的移动,对常数有所影响。

例题

D-query

查询不同点数,动态维护区间每个数出现次数即可。

这里我懒,所以使用了 map,导致复杂度多了个 \(\log\),但能过。不想要的话离散化即可。

code
int n,q;
int a[N],ans[Q];
map<int,int> cnt;
struct query{int l,r,id;}qs[Q];

int main(){
	read(n);
	int siz=sqrt(n);
	rep(i,1,n)read(a[i]);
	read(q);
	rep(i,1,q)read(qs[i].l),read(qs[i].r),qs[i].id=i;
	sort(qs+1,qs+1+q,[&](query a,query b){return (a.l/siz!=b.l/siz?a.l<b.l:((a.l/siz)&1?a.r>b.r:a.r<b.r));});
	int nl=1,nr=0,na=0;
	rep(i,1,q){
		while(nl<qs[i].l)na-=!--cnt[a[nl++]];
		while(nl>qs[i].l)na+=!cnt[a[--nl]]++;
		while(nr>qs[i].r)na-=!--cnt[a[nr--]];
		while(nr<qs[i].r)na+=!cnt[a[++nr]]++;
		ans[qs[i].id]=na;
	}
	rep(i,1,q)write(ans[i],'\n');
	return 0;
}

回滚莫队

实现

回滚莫队适用于加入操作与删除操作只有一个易于实现的时候。

既然只有一个可以实现,那就只实现一个。

这里以实现加入操作为例,删除操作同理。

同样的,我们将原序列分块,块长在时间复杂度部分分析,设作 \(b\)

然后同样对原序列排序:

  1. 左端点位于相同块的,按右端点升序排序。
  2. 左端点位于不同块的,按左端点升序排序。

我们逐块处理块内询问。到达一个新的块时,把需要清空的东西直接清空,维护的区间左端点移到块的右端点,然后维护的区间右端点移到左端点前一位,表示空序列。

依次处理询问,每次都把区间右端点移到查询的右端点位置,由于右端点单增,于是只需要加入。

但是,如果有查询的右端点位于区间右端点左侧怎么办?这时候查询区间长不超过 \(b\),直接整个区间跑一遍即可。

移完右端点后,先记录下当前区间的答案(因为需要回滚而不方便删除——这句话不理解的话可以先记着,马上讲了回滚部分就易于理解了),然后再移动左端点。

左端点是无序的,怎么办?我们每处理完一个询问,就把左端点回滚到当前块右端点位置,并且直接把答案设为之前记录的答案即可。

这样这一块下一次询问的时候,查询左端点必然在当前维护的区间左侧,仍然只需要加入操作。

复杂度

首先,回滚莫队的复杂度是 \(O(mb+\frac{n^2}{b})\) 的。

所以块长设作 \(\frac{n}{\sqrt{m}}\) 可以使复杂度为 \(O(n\sqrt m)\)

同样分段考虑。

排序

不影响复杂度,\(O(m\log m)\)

左右端点同块

每次跑一遍也就 \(b\) 次,最多跑 \(m\) 遍,复杂度 \(O(mb)\)

值得注意的是,每次清空的时候只清空区间内用得到的数据(且不要和不同块情况把数据存在一起),当然具体的影响看你到底维护了什么信息决定。

每次只清空区间内数据的话单次至多 \(O(b)\),整体还是 \(O(mb)\)

左右端点不同块

右端点移动 \(O(n)\),共 \(\frac{n}{b}\) 段,\(O(\frac{n^2}{b})\)

至于左端点,每次查询最多移动 \(b\) 次,\(O(mb)\)

进入新块

这个就看你维护什么信息了。最多也就移动 \(O(\frac{n}{b})\) 次,你清空信息顶多 \(O(n)\),因此至多 \(O(\frac{n^2}{b})\)

例题

歴史の研究

就是只支持加入的操作。没什么别的好讲的。

本来懒了,打算用 map,然后被卡,于是离散化。

code
int n,q,m;
int a[N],x[N];
ll ans[N];
ll cnt[N],cnt2[N];
struct query{int l,r,id;}qs[N];

int main(){
	read(n);read(q);
	int siz=ceil(n/sqrt(q));
	rep(i,1,n)read(a[i]),x[++m]=a[i];
	sort(x+1,x+1+m);m=unique(x+1,x+1+m)-x-1;
	rep(i,1,n)a[i]=lower_bound(x+1,x+1+m,a[i])-x;
	rep(i,1,q)read(qs[i].l),read(qs[i].r),qs[i].id=i;
	sort(qs+1,qs+1+q,[&](query a,query b){return a.l/siz!=b.l/siz?a.l<b.l:a.r<b.r;});
	int nl=-1,nr=0;ll na=0;
	rep(i,1,q){
		if(qs[i].l/siz==qs[i].r/siz){
			ll ta=0;
			rep(j,qs[i].l,qs[i].r)cnt2[a[j]]=0;
			rep(j,qs[i].l,qs[i].r)chmax(ta,x[a[j]]*++cnt2[a[j]]);
			ans[qs[i].id]=ta;
			continue;
		}else{
			int bl=qs[i].l/siz*siz,br=qs[i].l/siz*siz+siz-1;
			if(nl<bl){nl=br,nr=br-1,na=0;rep(i,1,m)cnt[i]=0;}
			while(nr<qs[i].r)++nr,chmax(na,x[a[nr]]*++cnt[a[nr]]);
			ll ba=na;
			while(qs[i].l<nl)--nl,chmax(na,x[a[nl]]*++cnt[a[nl]]);
			ans[qs[i].id]=na;
			while(nl<br)--cnt[a[nl++]];na=ba;
		}
	}
	rep(i,1,q)write(ans[i],'\n');
	return 0;
}

带修莫队

实现

众所周知地,莫队是一个离线算法。那么如何处理在线带修问题呢?

对于一些问题,我们可以使用带修莫队解决。

具体地,我们额外添加一条时间轴,原莫队算法是两个指针在一条横轴上移来移去,那么我们现在多维护一下当前所在的纵坐标,每次动态地让纵坐标移到当前查询所在的时间上。

其他实现都和普通莫队差不多,没有特别要讲的,简要说一下排序吧:

先按左端点分块,再按右端点分块,最后时间戳升序。(也可以像我一样奇偶块优化,但感觉效果不会很大。)

复杂度

首先结论:\(O(mb+\frac{n^2t}{b^2})\)\(t\) 表示纵轴长度,也就是时间戳个数。

这里我们假设 \(m,n,t\) 同阶,那么 \(b=n^\frac{2}{3}\) 时复杂度最优为 \(O(n^\frac{5}{3})\)

这里我们就省略排序等不重要部分了。

左端点

同普通莫队理,\(O(mb)\)

右端点

左端点分了 \(\frac{n}{b}\) 块,右端点也分了 \(\frac{n}{b}\) 块,和左端点同理,因此事实上,这部分也是 \(O(mb)\) 的。

纵轴

一共有 \((\frac{n}{b})^2\) 次(左端点右端点分块组合),每次依次上升,复杂度 \(O(t)\)

例题

数颜色

code
int n,m,q,t;
int cnt[M];
int c[N],fr[N],to[N],pc[N];
struct query{int l,r,t,id;}qs[N];
int ans[N];

int main(){
	read(n),read(m);
	rep(i,1,n)read(c[i]);
	rep(i,1,m){
		char o=reads()[0];int l=read(),r=read();
		if(o=='Q')qs[++q]={l,r,t,q};
		else ++t,fr[t]=c[l],to[t]=c[l]=r,pc[t]=l;
	}
	int siz=pow(n,2.0/3);
	sort(qs+1,qs+1+q,[&](query a,query b){return a.l/siz^b.l/siz?a.l<b.l:(a.r/siz^b.r/siz?(a.l/siz&1?a.r>b.r:a.r<b.r):(a.r/siz&1?a.t<b.t:a.t>b.t));});
	int nl=1,nr=0,nt=t,na=0;
	rep(i,1,q){
		while(nl<qs[i].l)na-=!--cnt[c[nl++]];
		while(qs[i].r<nr)na-=!--cnt[c[nr--]];
		while(qs[i].l<nl)na+=!cnt[c[--nl]]++;
		while(nr<qs[i].r)na+=!cnt[c[++nr]]++;
		while(nt<qs[i].t)++nt,na-=nl<=pc[nt]&&pc[nt]<=nr&&!--cnt[fr[nt]],na+=nl<=pc[nt]&&pc[nt]<=nr&&!cnt[to[nt]]++,c[pc[nt]]=to[nt];
		while(nt>qs[i].t)na-=nl<=pc[nt]&&pc[nt]<=nr&&!--cnt[to[nt]],na+=nl<=pc[nt]&&pc[nt]<=nr&&!cnt[fr[nt]]++,c[pc[nt]]=fr[nt],--nt;
		ans[qs[i].id]=na;
	}
	rep(i,1,q)write(ans[i],'\n');
	return 0;
}

莫队二次离线

首先放一道莫队二次离线例题

我们根据这道例题讲解莫队二次离线。

莫队一次离线,就是离线所有询问。而二次离线,就是离线端点的所有移动。

以这道题为例,移动一次端点,就会给答案更改区间内所有大于或小于这个数的数个数,记查询区间为 \([l,r]\),那么可以差分为 \([1,r]\) 的答案减去 \([1,l-1]\) 的答案,前者是简单的,考虑后者。(这是移动右端点的情况,移动左端点同理)

考虑把每次移动视作一次查询,挂到 \(l-1\) 上(移动左端点就挂到 \(r+1\) 上)。那么就可以离线下来扫描线,修改 \(O(n)\) 次,查询 \(O(n\sqrt m)\) 次(就是莫队移动的复杂度)。那么值域分块可以做到单次修改 \(O(\sqrt n)\)、查询 \(O(1)\)\(n,m\) 同阶复杂度即为 \(O(n\sqrt n)\)。当然空间开不下,对每次移动,存端点移动的的区间端点即可,只会进行 \(m\) 次移动,那空间复杂度就是 \(O(m)\) 的。

code
const int N=1e5+5;

#define getchar getchar_unlocked
#define putchar putchar_unlocked
template<typename Type> inline void read(Type &x){x=0;char ch=getchar();while(!isdigit(ch)&&~ch)ch=getchar();while(isdigit(ch))x=x*10+ch-48,ch=getchar();}
template<typename Type> inline void write(Type x){if(x>9)write(x/10);putchar(x%10+'0');}
template<typename Type,typename...T> inline void read(Type &x,T&...y){read(x),read(y...);}

int n,m;
int a[N];
int x[N],X;
struct Query{int l,r,id;}qs[N];
namespace BIT{
    int dat[N];
    inline void modify(int x){for(;x<=X;x+=x&-x)++dat[x];};
    inline int query(int x){int res=0;for(;x;x-=x&-x)res+=dat[x];return res;}
}
ll ansl[N],ansr[N],ans[N];
struct Query2{int kd,id,l,r;};
vec<Query2> ql[N],qr[N];
namespace SQRT{
    int B;
    int L[N],R[N],bid[N];
    int dat[N],laz[N];
    inline void modify(int lq,int rq){
        if(lq>rq)return;
        int lb=bid[lq],rb=bid[rq];
        if(lb==rb){
            rep(i,L[lb],R[lb])dat[i]+=laz[lb];
            laz[lb]=0;
            rep(i,lq,rq)++dat[i];
        }else{
            if(lq==L[lb])++laz[lb];
            else{
                rep(i,L[lb],R[lb])dat[i]+=laz[lb];
                laz[lb]=0;
                rep(i,lq,R[lb])++dat[i];
            }
            if(rq==R[rb])++laz[rb];
            else{
                rep(i,L[rb],R[rb])dat[i]+=laz[rb];
                laz[rb]=0;
                rep(i,L[rb],rq)++dat[i];
            }
            rep(b,lb+1,rb-1)++laz[b];
        }
    }
    inline int query(int k){return dat[k]+laz[bid[k]];}
}

signed main(){
    read(n,m);
    rep(i,1,n)read(a[i]),x[i]=a[i];
    sort(x+1,x+1+n);X=unique(x+1,x+1+n)-x-1;
    rep(i,1,n)a[i]=lower_bound(x+1,x+1+X,a[i])-x;
    rep(i,1,n)ansr[i]=ansr[i-1]+BIT::query(X-(a[i]+1)+1),BIT::modify(X-a[i]+1);
    rep(i,1,X)BIT::dat[i]=0;
    per(i,n,1)ansl[i]=ansl[i+1]+BIT::query(a[i]-1),BIT::modify(a[i]);
    rep(i,1,m)cin>>qs[i].l>>qs[i].r,qs[i].id=i;
    int B=n/(sqrt(m)+1)+1;
    sort(qs+1,qs+1+m,[&](Query a,Query b){if(a.l/B!=b.l/B)return a.l<b.l;return a.r<b.r;});
    int L=1,R=1;
    rep(i,1,m){
        if(R<qs[i].r)ans[qs[i].id]+=ansr[qs[i].r]-ansr[R],ql[L-1].pub({-1,qs[i].id,R+1,qs[i].r}),R=qs[i].r;
        if(qs[i].l<L)ans[qs[i].id]+=ansl[qs[i].l]-ansl[L],qr[R+1].pub({-1,qs[i].id,qs[i].l,L-1}),L=qs[i].l;
        if(qs[i].r<R)ans[qs[i].id]-=ansr[R]-ansr[qs[i].r],ql[L-1].pub({1,qs[i].id,qs[i].r+1,R}),R=qs[i].r;
        if(L<qs[i].l)ans[qs[i].id]-=ansl[L]-ansl[qs[i].l],qr[R+1].pub({1,qs[i].id,L,qs[i].l-1}),L=qs[i].l;
    }
    SQRT::B=sqrt(X)+1;
    int t=(X-1)/SQRT::B+1;
    rep(i,1,t)SQRT::R[i]=i*SQRT::B,SQRT::L[i]=SQRT::R[i]-SQRT::B+1;
    SQRT::R[t]=X;
    rep(i,1,t)rep(j,SQRT::L[i],SQRT::R[i])SQRT::bid[j]=i;
    rep(i,1,n){
        SQRT::modify(1,a[i]-1);
        for(auto q:ql[i]){
            ll res=0;
            rep(i,q.l,q.r)res+=SQRT::query(a[i]);
            ans[q.id]+=q.kd*res;
        }
    }
    rep(i,1,X)SQRT::dat[i]=0;
    rep(i,1,t)SQRT::laz[i]=0;
    per(i,n,1){
        SQRT::modify(a[i]+1,X);
        for(auto q:qr[i]){
            ll res=0;
            rep(i,q.l,q.r)res+=SQRT::query(a[i]);
            ans[q.id]+=q.kd*res;
        }
    }
    rep(i,1,m)ans[qs[i].id]+=ans[qs[i-1].id];
    rep(i,1,m)write(ans[i]),putchar('\n');
	return 0;
}
posted @ 2025-05-24 14:50  LastKismet  阅读(19)  评论(0)    收藏  举报