莫隊詳解


莫隊實際很簡(du)單(liu)

依照某位dalao的說法,就是兩只小手(two-pointers)瞎跳

一.莫隊(靜態莫隊)

我們以Luogu P3901 數列找不同為例講一下靜態莫隊

這道題是個綠題,因為數據比較弱,但真是一道良心的莫隊練手題

莫隊是由前國家隊隊長莫濤發明的

莫隊算法的精髓就是通過合理地對詢問排序,然后以較優的順序暴力回答每個詢問。處理完一個詢問后,可以使用它的信息得到下一個詢問區間的答案。(兩個小手瞎跳)

考慮這個問題:對於上面這道題,我們知道區間[1,5]每個數的數量,如何求出[2,6]每個數的數量

算法1:暴力掃一遍(廢話)

算法2:珂以在區間[1,5]的基礎上,去掉位置1(即將左端點右移一位),加上位置6(即將右端點右移一位),得到區間[2,6]的答案。

如果按這樣寫,一種很簡單的構造數據就能把時間復雜度把算法2也送上天:先詢問[1,2],再詢問[99999,100000],多重復幾次就gg

但莫隊算法是算法2的改進版

要進行合理的排序,使得每兩個區間的距離最小

但如何進行合理的排序?

莫隊提供了這樣一個排序方案:將原序列以$ \sqrt n$為一塊進行分塊(分塊的大小也珂以調整),排序第一關鍵字是詢問的左端點所在塊的編號,第二關鍵字是詢問的右端點本身的位置,都是升序。然后我們用上面提到的“移動當前區間左右端點”的方法,按順序求每個詢問區間的答案,移動每一個詢問區間左右端點可以求出下一個區間的答案。

這就是一般的莫隊排序

inline bool cmp(register query a,register query b)
{
	return a.bl==b.bl?a.r<b.r:a.bl<b.bl;
}

但由於出題人過於毒瘤

又多出一種優化,叫做奇偶優化

按奇偶塊排序。這也是比較通用的。如果區間左端點所在塊不同,那么就直接按左端點從小到大排;如果相同,奇塊按右端點從小到大排,偶塊按右端點從大到小排。

inline bool cmp(register query a,register query b)
{
    return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
}

莫隊核心代碼qaq:

sort(q+1,q+m+1,cmp); //講詢問按上述方法排序 
int l=1,r=0; //當前左端點和右端點初值(兩只小手two-pointers) 
for(register int i=1;i<=m;++i) //對排序后的詢問一個個轉移 
{
	int ll=q[i].l,rr=q[i].r; //本次詢問的區間 
	//轉移,++--這些東西比較容易寫錯,需要注意 
	while(l<ll)
		del(l++);
	while(l>ll)
		add(--l);
	while(r<rr)
		add(++r);
	while(r>rr)
		del(r--);
	ans[q[i].id]=sth; //詢問是排過序的,存到答案數組里需要返回原順序 
}

這樣就可以求出答案了!

——可是,這樣做的復雜度是什么?

大約是\(O(n \sqrt n)\)

Luogu P3901 AC代碼:

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define N 100005
using namespace std;
inline int read()
{
	register int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
int v[N],blocksize=0;
struct query{
	int l,r,id,bl;
}q[N];
int sum[N];
bool ans[N];
int cnt=0;
inline void add(register int x)
{
	if(++sum[v[x]]==1)
		++cnt; 
}
inline void del(register int x)
{
	if(--sum[v[x]]==0)
		--cnt;
}
inline bool cmp(register query a,register query b)
{
    return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
}
int main()
{
	memset(sum,0,sizeof(sum));
	int n=read(),m=read();
	blocksize=sqrt(n);
	for(register int i=1;i<=n;++i)
		v[i]=read();
	for(register int i=1;i<=m;++i)
	{
		int l=read(),r=read();
		q[i]=(query){l,r,i,(l-1)/blocksize+1};
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(register int i=1;i<=m;++i)
	{
		int ll=q[i].l,rr=q[i].r;
		while(l<ll)
			del(l++);
		while(l>ll)
			add(--l);
		while(r<rr)
			add(++r);
		while(r>rr)
			del(r--);
		ans[q[i].id]=(cnt==rr-ll+1)?1:0;
	}
	for(register int i=1;i<=m;++i)
		if(ans[i])
			puts("Yes");
		else
			puts("No");
	return 0;
 } 

例題:

1.Luogu P3901 數列找不同

講解比上面暴力

2.Luogu CF375D Tree and Queries

樹鏈剖分(dfs序)后跑莫隊

3.Luogu SP3267 DQUERY - D-query

莫隊入門題

4.Luogu P1972 [SDOI2009]HH的項鏈

上面那道題略加卡常

5.Luogu CF86D Powerful array

莫隊與小學數學

6.Luogu P1533 可憐的狗狗

莫隊+平衡樹苟過

7.Luogu P5072 [Ynoi2015]盼君勿忘

題面好評,莫隊模板,只是在算貢獻時稍微毒瘤

7.Luogu P5071 [Ynoi2015]此時此刻的光輝

題面好評,莫隊模板和pollard's Rho的結合

二.動態莫隊(單點修改)

寫完了上面這道題,可以發現:普通的莫隊算法沒有支持修改。那么如何改造該算法使它支持修改呢?

莫隊俗稱優雅的暴力

我們以Luogu P1903 [國家集訓隊]數顏色 / 維護隊列講解一下動態莫隊

那么我們改造莫隊算法的思路也只有一個:改造詢問排序的方式,然后繼續暴力。

首先我們需要把查詢操作和修改操作分別記錄下來。

在記錄查詢操作的時候,需要增加一個變量來記錄離本次查詢最近的修改的位置

然后套上莫隊的板子,與普通莫隊不一樣的是,你需要用一個變量記錄當前已經進行了幾次修改

每次回答詢問時,先從上一個詢問的時間“穿越”到當前詢問的時間:如果當前詢問的時間更靠后,則順序執行所有修改,直到達到當前詢問時間;如果當前詢問的時間更靠前,則“時光倒流”,還原所有多余的修改。進行推移時間的操作時,如果涉及到當前區間內的位置的修改,要對答案進行相應的維護。

排序有三個關鍵字:

1.左端點所在塊數 2.右端點所在塊數 3.在這次修改之前查詢的次數

inline bool cmp(register query a,register query b)
{
	return a.bll!=b.bll?a.bll<b.bll:(a.blr!=b.blr?a.blr<b.blr:a.pre<b.pre);
}

完整代碼,代碼中有詳細注釋

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define N 50005
using namespace std;
inline int read()
{
	register int x=0,f=1;register char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
inline void write(register int x)
{
	if(!x)putchar('0');if(x<0)x=-x,putchar('-');
	static int sta[25];int tot=0;
	while(x)sta[tot++]=x%10,x/=10;
	while(tot)putchar(sta[--tot]+48);
}
struct change{    //記錄修改操作的結構體,place為修改的位置,pre是修改之前的值,suf是修改之后的值 
	int place,pre,suf;
}cg[N];
struct query{     //記錄查詢操作的結構體,l,r為查詢左右端點,pre表示之前有過幾次修改,id表示這是第幾次查詢,bll,blr表示左右端點所在的塊 
	int l,r,pre,id,bll,blr;
}q[N];
int a[N],blocksize=0,p[1000001],ans[N];
inline bool cmp(register query a,register query b) //按上述三個關鍵字排序 
{
	return a.bll!=b.bll?a.bll<b.bll:(a.blr!=b.blr?a.blr<b.blr:a.pre<b.pre);
}
int main()
{
	int n=read(),m=read(),tota=0,totb=0;
	for(register int i=1;i<=n;++i)
		a[i]=read();
	for(register int i=1;i<=m;++i)
	{
		char ch=getchar();
		while(ch!='R'&&ch!='Q')
			ch=getchar();
		if(ch=='R') //修改 
		{
			cg[++tota].place=read(),cg[tota].suf=read();
			cg[tota].pre=a[cg[tota].place]; //為了方便先在原數組上修改 
			a[cg[tota].place]=cg[tota].suf;
		}
		else
		{
			int l=read(),r=read();
			q[++totb]=(query){l,r,tota,totb,0};
		}
	}
	blocksize=ceil(exp((log(n)+log(tota))/3)); //奇妙的塊的大小 
	for(register int i=1;i<=totb;++i)
		q[i].bll=(q[i].l-1)/blocksize+1,q[i].blr=(q[i].r-1)/blocksize+1;
	for(register int i=tota;i>=1;--i) //還原數組 
		a[cg[i].place]=cg[i].pre;
	sort(q+1,q+totb+1,cmp); //排序 
	int l=1,r=0,num=0,ti=0;
	for(register int i=1;i<=m;++i)
	{
		int ll=q[i].l,rr=q[i].r,t=q[i].pre;
	    //正常莫隊操作 
		while(ll<l)
			num+=!p[a[--l]]++;
		while(ll>l)
			num-=!--p[a[l++]];
		while(rr>r)
			num+=!p[a[++r]]++;
		while(rr<r)
			num-=!--p[a[r--]];
		while(t<ti) //當本次查詢時修改的次數小於已經修改的次數,時光倒流 (還原修改) 
		{
			int pla=cg[ti].place;
			if(l<=pla&&pla<=r)
				num-=!--p[a[pla]];
			a[pla]=cg[ti--].pre;
			if(l<=pla&&pla<=r)
				num+=!p[a[pla]]++;
		}
		while(t>ti) //當本次查詢時修改的次數大於已經修改的次數,穿越 (把該修改的修改) 
		{
			int pla=cg[++ti].place;
			if(l<=pla&&pla<=r)
				num-=!--p[a[pla]];
			a[pla]=cg[ti].suf;
			if(l<=pla&&pla<=r)
				num+=!p[a[pla]]++;
		}
		ans[q[i].id]=num;
	}
	for(register int i=1;i<=totb;++i)
	{
		write(ans[i]);
		printf("\n");
	}
	return 0;
}

三、樹上莫隊

樹上莫隊,顧名思義就是把莫隊搬到樹上。

復雜度同序列上的莫隊(不帶修:\(O(n \sqrt n)\),帶修:\(O(n^\frac{5}{3})\)

我們根據Luogu SP10707 COT2 - Count on a tree II來講樹上莫隊

題目意思很明確:給定一個n個節點的樹,每個節點表示一個整數,問u到v的路徑上有多少個不同的整數。

像這種不帶修改數顏色的題首先想到的肯定是樹套樹莫隊,那么如何把在序列上的莫隊搬到樹上呢?

歐拉序

我們考慮用什么東西可以把樹上的問題轉化到序列上,dfs序是可以的,但是這道題不行(無法搞lca的貢獻)

有一種神奇的東西,叫做歐拉序。

它的核心思想是:當訪問到點i時,加入序列,然后訪問i的子樹,當訪問完時,再把i加入序列

煮個栗子,下面這棵樹的歐拉序為

1 2 3 4 4 5 5 6 6 3 7 7 2 1

1101696-20180625111030297-903825718.png

有了這個有什么用呢?

我們考慮我們要解決的問題:求x到y的路徑上有多少個不同的整數

這里我們設st[i]表示訪問到i時加入歐拉序的時間,ed[i]表示回溯經過i時加入歐拉序的時間

不妨設st[x]<st[y](也就是先訪問x,再訪問y)

分情況討論

  • 若lca(x,y)=x,這時x,y在一條鏈上,那么st[x]到st[y]這段區間中,有的點出現了兩次,有的點沒有出現過,這些點都是對答案沒有貢獻的,我們只需要統計出現過1次的點就好
    比如當詢問為2,6時,(st[2],st[6])=2 3 4 4 5 5 6 4,5這兩個點都出現了兩次,因此不統計進入答案
  • 若lca(x,y)≠x,此時x,y位於不同的子樹內,我們只需要按照上面的方法統計ed[x]到st[y]這段區間內的點。
    比如當詢問為4,7時,(ed[4],st[7])=4 5 5 6 6 3 7。大家發現了什么?沒錯!我們沒有統計lca,因此我們需要特判lca

算歐拉序之后可以順帶重鏈剖分,這樣lca就直接樹剖來求了qaq

完整代碼

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define N 40005
#define M 100005
#define getchar nc
using namespace std;
inline char nc(){
    static char buf[100000],*p1=buf,*p2=buf; 
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++; 
}
inline int read()
{
    register int x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register int x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[20];register int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
inline void Swap(register int &a,register int &b)
{
    a^=b^=a^=b;
}
struct edge{
    int to,next;
}e[N<<1];
int head[N],cnt=0;
inline void add(register int u,register int v)
{
    e[++cnt]=(edge){v,head[u]};
    head[u]=cnt;
}
int n,m;
int a[N],date[N];
int dep[N],top[N],siz[N],son[N],fa[N],st[N],ed[N],pot[N<<1],tot=0;
inline void dfs1(register int x,register int f)
{
    fa[x]=f,siz[x]=1,st[x]=++tot;
    pot[tot]=x;
    for(register int i=head[x];i;i=e[i].next)
        if(e[i].to!=f)
        {
            dep[e[i].to]=dep[x]+1;
            dfs1(e[i].to,x);
            siz[x]+=siz[e[i].to];
            if(siz[e[i].to]>siz[son[x]])
                son[x]=e[i].to;
        }
    ed[x]=++tot;
    pot[tot]=x;
}
inline void dfs2(register int x,register int topf)
{
    top[x]=topf;
    if(son[x])
        dfs2(son[x],topf);
    for(register int i=head[x];i;i=e[i].next)
        if(e[i].to!=fa[x]&&e[i].to!=son[x])
            dfs2(e[i].to,e[i].to);
}
inline int Getlca(register int x,register int y)
{
    while(top[x]!=top[y])
    {
        if(dep[top[x]]<dep[top[y]])
            Swap(x,y);
        x=fa[top[x]];
    }
    return dep[x]<dep[y]?x:y;
}
struct query{
    int l,r,id,lca;
}q[M];
int bel[N<<1],block;
inline bool cmp(register query a,register query b)
{
    return bel[a.l]!=bel[b.l]?a.l<b.l:((bel[a.l]&1)?a.r<b.r:a.r>b.r);
}
int ans[M],p[N],vis[N],res=0;
inline void add(register int x)
{
    res+=!p[x]++;
}
inline void del(register int x)
{
    res-=!--p[x];
}
inline void Add(register int x)
{
    vis[x]?del(a[x]):add(a[x]);
    vis[x]^=1;
}
int main()
{
    n=read(),m=read();
    block=sqrt(n);
    for(register int i=1;i<=n;++i)
        a[i]=date[i]=read();
    for(register int i=1;i<=(n<<1);++i)
        bel[i]=i/block+1;
    sort(date+1,date+1+n);
    int num=unique(date+1,date+1+n)-date-1;
    for(register int i=1;i<=n;++i)
        a[i]=lower_bound(date+1,date+1+num,a[i])-date;
    for(register int i=1;i<n;++i)
    {
        int u=read(),v=read();
        add(u,v),add(v,u);
    }
    dep[1]=1;
    dfs1(1,0);
    dfs2(1,1);
    for(register int i=1;i<=m;++i)
    {
        int x=read(),y=read();
        if(st[x]>st[y])
            Swap(x,y);
        int lcaa=Getlca(x,y);
        q[i].id=i;
        if(lcaa==x)
            q[i].l=st[x],q[i].r=st[y];
        else
            q[i].l=ed[x],q[i].r=st[y],q[i].lca=lcaa;
    }
    sort(q+1,q+1+m,cmp);
    int l=1,r=0;
    for(register int i=1;i<=m;++i)
    {
        int ll=q[i].l,rr=q[i].r;
        while(l<ll)
            Add(pot[l++]);
        while(l>ll)
            Add(pot[--l]);
        while(r<rr)
            Add(pot[++r]);
        while(r>rr)
            Add(pot[r--]);
        if(q[i].lca)
            Add(q[i].lca);
        ans[q[i].id]=res;
        if(q[i].lca)
            Add(q[i].lca);
    }
    for(register int i=1;i<=m;++i)
        write(ans[i]),puts("");
    return 0;
} 

四、回滾莫隊

1.我們以塊編號為第一關鍵字排序,右端點位置為第二關鍵字排序

2.詢問時依次枚舉區間,我們保留右端點的移量(右邊單增),左端點則每次在這一個塊中來回移動

3.下一個塊時,清空統計答案重做

所以對於每一個塊:左端點每次操作\(O(\sqrt n)\),右端點總共移n,均攤\(O(\sqrt n)\),因此時間復雜度保證了\(O(n\sqrt n)\)

完整代碼Luogu AT1219 歴史の研究

#include <bits/stdc++.h>
#define N 100005
#define ll long long
using namespace std;
inline ll read()
{
    register ll x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register ll x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[36];int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
inline ll Max(register ll a,register ll b)
{
	return a>b?a:b;
}
struct query{
	int l,r,id,bll,blr;
}q[N];
inline bool cmp(register query a,register query b)
{
    return a.bll==b.bll?a.r<b.r:a.bll<b.bll;
}
int n,m,blocksize;
int a[N],v[N];
ll sum[N],num[N],ans[N];
ll pre,now;
inline void add(register int x)
{
	sum[x]+=v[x];
	now=Max(now,sum[x]);
}
inline void del(register int x)
{
	sum[x]-=v[x];
}
int main()
{
	n=read(),m=read();
	blocksize=sqrt(n);
	for(register int i=1;i<=n;++i)
		v[i]=a[i]=read();
	sort(v+1,v+1+n);
	int tot=unique(v+1,v+1+n)-v-1;
	for(register int i=1;i<=n;++i)
		a[i]=lower_bound(v+1,v+1+tot,a[i])-v;
	for(register int i=1;i<=m;++i)
	{
		int l=read(),r=read();
		q[i]=(query){l,r,i,(l-1)/blocksize+1,(r-1)/blocksize+1};
	}
	sort(q+1,q+1+m,cmp);
	int l=1,r=0,pos=0;
	for(register int i=1;i<=m;++i)
	{
		int ql=q[i].l,qr=q[i].r;
		if(q[i].bll!=q[i-1].bll)
		{
			memset(sum,0,sizeof(sum));
			pre=now=0;
			l=pos=q[i].bll*blocksize+1;
			r=l-1;
		}
		if(q[i].bll==q[i].blr)
		{
			ll cur=0;
			for(register int j=ql;j<=qr;++j)
			{
				num[a[j]]+=v[a[j]];
				cur=Max(cur,num[a[j]]);
			}
			for(register int j=ql;j<=qr;++j)
				num[a[j]]-=v[a[j]];
			ans[q[i].id]=cur;
			continue;
		}
		while(r<qr)
			add(a[++r]);
		pre=now;
		while(l>ql)
			add(a[--l]);
		ans[q[i].id]=now;
		while(l<pos)
			del(a[l++]);
		now=pre;
	}
	for(register int i=1;i<=m;++i)
		write(ans[i]),puts("");
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM