莫隊算法


莫隊算法

基本莫隊算法介紹

莫隊算法是一個分塊算法,是由某國家集訓隊大佬提出的一個算法,我們就用一道 SDOI2009 的題來引入吧。

HH的項鏈

大概概括一下題意就是我們給定一個長度為 \(n\) 的自然數數列 \(a_1,a_2,a_3......a_{n-1},a_n\)。有 \(q\) 次詢問,每次給出 \(l,r\),我們需要回答在 \([l,r]\) 內有多少不同的數。其中,\(n\le 5\cdot 10^4,m\le 2\cdot 10^5,a_i\le 10^6\)(這是原數據的范圍,洛谷上有所加強)。

這就是題目的樣例,那么我們知道,最暴力的方法就是對於每一次詢問清空累加數組,每次都重新統計,時間復雜度為 \(O(q\cdot n)\)

那么我們再加一個小優化,我們知道,對於第 \(2,3\) 次詢問來說,他們有重復部分 \([3,5]\)。那么這個部分我們就不用重復處理,只需要加上 \([2,2],[6,6]\) 兩個區間即可。

但是這樣時間復雜度依然還是 \(O(q\cdot n)\)。我們如果造了這樣一組數據,使得相鄰兩次的詢問區間都沒有交集,這樣的時間復雜度反而還不如之前的暴力(指常數)。因此,我們一定需要一種較為穩定,且時間復雜度小的一種算法,也就是莫隊。

眾所周知,莫隊是一個離線的算法。我們先對詢問排序,這樣可以讓算法的時間復雜度大大退化,達到 \(O(q\cdot \sqrt{n}+ n\cdot \sqrt{n})\)

莫隊算法引用了分塊的思想,我們定義:一個連續的數列稱為一個塊。 一個塊的長度就是這個塊所包含的數的個數。所以一個長度為 \(n\) 的數列可以分成至少 \(\lfloor\frac{n}{\lfloor \sqrt{n}\rfloor}\rfloor\) 個長度為 \(\lfloor \sqrt{n}\rfloor\) 的塊,還有一個長度小於 \(\sqrt{n}\) 的塊(這個塊的長度可以為0)。注意我們接下來的討論全部忽視這個小塊,其涉及到的常數問題不討論,且認為 \(\lfloor\frac{n}{\lfloor \sqrt{n}\rfloor}\rfloor=\sqrt{n}\) 這會大大減少我的Latex公式編輯量。這樣我們的時間復雜度和后續一些問題會比較好理解一些。畢竟這點誤差還是在我們的接受范圍內的。除非你要做 POJ 上的題,老爺機會讓你崩潰的

咳咳,我們回到正題。我們說一下排序的方式,然后再說一下為什么這樣排序。我們先把數列分成 \(\sqrt{n}\) 個 長度為 \(\sqrt{n}\) 的塊,如圖。再說一遍:這里認為 \(\sqrt{n}=2\),但是這里有三個塊,我們后面仍然認為只有 \(\sqrt{n}\)

分好塊了,我們現在說一下排序的細則:我們以詢問區間的左端點 \(l\) 所在的塊的編號為第一關鍵字,以右端點 \(r\) 的編號為第二關鍵字進行排序(注意有些人學的莫隊是以右端點 \(r\) 所在的塊的編號為第二關鍵字的,這個在部分情況下會有略微差別,一般認為是卡常,因為時間復雜度差別不是很大,本文中無說明情況下,以 \(r\) 的編號為第二關鍵字)。對樣例進行完了排序后就是這樣的一幅圖:

可能這個樣例看着不是很明顯優化在哪里,但是如果我們來推算一下時間復雜度,就知道這個算法的優點了。

我們首先考慮左端點的移動情況,由於我們排過序,所有在一個塊內的移動最多 \(\sqrt{n}\),因為一個塊的長度為 \(\sqrt{n}\)。如果是跨塊的移動,可能為 \(n\),但這是unsustainable不可持續的,一旦移動到了下一個塊中,就不會后移,所以跨塊操作的平均情況應該是 \(\sqrt{n}\)。所以左端點的移動復雜度就為 \(O(q\sqrt{n})\)
我們再來看右端點,對於右端點來說,她是按編號排的序。所以對於左端點在於一個塊中的詢問來說,右端點的極端情況是從 \(1\) 一直移動到 \(n\)。我們總共有 \(\sqrt{n}\) 個塊,所以與端點的比較好估算,是 \(O(n\sqrt{n})\)
所以莫隊算法的總時間復雜度就是 \(O(n\sqrt{n}+q\sqrt{n})\),可以通過此題。但由於洛谷上此題數據有所加強,無法使用莫隊通過,需要用樹狀數組的方法來做,這里不再贅述這個問題。莫隊的代碼見下:

想要測這道題的同學可以到這里來測試

#include <bits/stdc++.h>

using namespace std;

const int maxn=5e4+10;

struct pr {
	int l,r,id,bel;
}q[maxn*20];

int ans[maxn*20],a[maxn*20],n,q,num[1000010],cur;

bool cmp(pr x,pr y) {
	if(x.bel!=y.bel) {
		return x.bel<y.bel;
	}
	return x.r<y.r;
}

void add(int x) {
	num[x] ++;
	if (num[x] == 1)
		cur ++;
}

void del(int x) {
	num[x] --;
	if (num[x] == 0)
		cur --;
}

int main() {
	
	cin >> n;
	int blocks = sqrt(n);
	
	for (int i = 1; i <= n; ++i) {
		scanf("%d", &a[i]);
	}
	cin >> q;
	for (int i = 1; i <= q; ++i) {
		q[i].id = i;
		scanf("%d %d", &q[i].l, &q[i].r);
		q[i].bel = (q[i].l - 1) / blocks  + 1;//計算左端點所在的塊
	}
	sort(q + 1, q + q + 1, cmp);
	
	
	int l = 1, r = 0;
	
	for (int i = 1; i <= q; ++i) {
		while (l < q[i].l) del(a[l++]);//這些是 l,r 的移動
		while (l > q[i].l) add(a[--l]);
		while (r < q[i].r) add(a[++r]);
		while (r > q[i].r) del(a[r--]);
		ans[q[i].id] = cur;//統計情況;
	}
	
	for (int i =1 ; i <= q; ++i) {
		printf("%d\n",ans[i]);
	}
	return 0;
}

但可能小伙伴們還有一些問題,比如說一下兩個最有代表性:

Q1: 為什么選取塊的長度為 \(\sqrt{n}\)
A1: 解答這個問題,我們要先搞清一點,\(q\sqrt{n},n\sqrt{n}\) 中的 \(\sqrt{n}\) 含義不同。一個是塊長,一個是塊的個數。我們現在設塊長為 \(k\)。那么時間復雜度為 \(O(qk+n\cdot \frac{n}{k})\)。可以用基本不等式得到:\(qk+n\cdot \frac{n}{k}\geq 2\sqrt{qn^2}\),也可以知道當 \(qk=n\cdot \frac{n}{k}\) 時,即 \(k=\frac{n\sqrt{q}}{q}\) 時,可以取到最小值 \(2\sqrt{qn^2}\)。所以最理想的塊長為 \(\frac{n\sqrt{q}}{q} \approx \sqrt{n}\),時間復雜度最小為 \(2\sqrt{qn^2}\approx n\sqrt{n}+q\sqrt{n}\)

我們可以來測一下這兩種的區別:

\(\sqrt{n}\) 版:

\(\frac{n\sqrt{q}}{q}\) 版:

可見沒什么大區別。所以我們一般采取長度為 \(\sqrt{n}\) 的塊即可。

Q2: 為什么要分塊,為何不能以左端點的編號為第一關鍵字?
A2: 我們可以這樣想,以左端點為第一關鍵字的話,相當於塊的長度 \(k=1\)。根據上面的推導知道這樣時間復雜度是 \(O(q+n^2)\)。所以不可以的。

[國家集訓隊]小Z的襪子

那么我們再來看這樣一道題。這道題明顯是上一題的一個變形,只是我們要把上面的 \(\operatorname{add},\operatorname{del}\) 函數稍做調整。\(\operatorname{add}\) 就是把目前相同襪子顏色對答案貢獻加上,把原來的減掉,\(\operatorname{del}\) 也一樣。我們就可以得到以下代碼:

void add(int x) {
	cur-=num[x]*(num[x]-1);
	num[x] ++;
	cur+=num[x]*(num[x]-1);
}

void del(int x) {
	cur-=num[x]*(num[x]-1);
	num[x] --;
	cur+=num[x]*(num[x]-1);
}

注意分數要約分即可。

帶修莫隊

莫隊這種算法,一定會有修改呀。如果我們在查詢的時候有修改,怎么辦呢?我們的詢問順序打亂了,怎么處理修改呢?

[國家集訓隊]數顏色

我們還是以一道例題引入。題目自己看一看,我把樣例的圖放上來,具體就不解釋了:

注意,我們這里改一改原來題目中的字母,在代碼中不改。我們定義有 \(k\) 次操作,有 \(m\) 個修改操作,有 \(q\) 個詢問操作。所以有 \(m+q=k\)

其實我們很容易想到,我們記錄一下每次詢問前有多少次修改操作,因為我們不改變修改的順序,這樣,我們每次還要記錄一下當前維護的區間進行了幾次修改,然后我們只需要根據這些信息,可以把已進行的修改改回去,或者再繼續改下去。

這道題洛谷的數據有所加強,原數據評測請到這里

慢着,這樣就可以了嗎?這樣可以通過原數據,但是在洛谷上加強的數據無法通過。所以我們要想辦法繼續優化我們的算法。我們先來算一算時間復雜度。

左端點和右端點的移動仍然相同。我們算一下修改的時間復雜度。我們完全可以設計一種數據,先進行 \(\frac{q}{2}\) 次詢問,然后集中進行 \(m\) 次修改,最后在把之前的詢問再問一遍,這樣你每次就要來回的修改,時間復雜度為 \(O(qm)\)。總時間復雜度為 \(O(q\sqrt{n}+n\sqrt{n}+qm)\)。更極限情況下,\(q=m=\frac{n}{2}\)。這樣時間復雜度為 \(O(\frac{3\cdot n\sqrt{n}}{2}+\frac{n^2}{4})\)。顯然炸掉。

我們的已解決辦法就是來改一下塊長,故技重施。我們還是取最極端的情況 \(q=m=\frac{n}{2}\)。此時設塊長為 \(l\)\(k\) 已經有意義了,所以換一個參數),最好的 \(l=\sqrt[3]{n^2}\)。這樣就可以通過此題。另外,此題最好以右端點 \(r\) 所在塊為第二關鍵字。這樣在本題中發揮更好。(最后就是要加上一個火車頭,開個 O2 才能通過)。

#pragma GCC diagnostic error "-std=c++11"
#pragma GCC target("avx")
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")//壯觀
#include<bits/stdc++.h>
#define int long long
using namespace std;

int read() {
	char ch=getchar();
	int f=1,x=0;
	while(ch<'0'||ch>'9') {
		if(ch=='-')
			f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9') {
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}

const int maxn=133334;

struct query {
	int l,r,id,bell,belr,num;
}q[maxn];

struct modify {
	int x,val,pre;
}c[maxn]; 

bool cmp(query x,query y) {
	if(x.bell!=y.bell) {
		return x.bell<y.bell;
	}
	if(x.belr!=y.belr)
		return x.belr<y.belr;
	return x.num<y.num;
}

int n,ans[maxn],m,a[maxn],cur,b[maxn],num[1000010];

void add(int x) {
	num[x]++;
	if(num[x]==1) {
		cur++;
	}
}
void del(int x){
	num[x]--;
	if(num[x]==0) {
		cur--;
	}
}

signed main() {

	n=read();m=read();
	int block=pow(n,2.0/3),mnum=0,cntq=0,cntm=0;
	fill(ans,ans+1+m,-1);
	
	for(int i=1;i<=n;i++) {
		a[i]=read();
		b[i]=a[i];
	}
	for(int i=1;i<=m;i++) {
		char s;
		cin>>s;
		if(s=='Q') {
			q[++cntq].l=read();
			q[cntq].r=read();
			q[cntq].bell=(q[cntq].l-1)/block+1;
			q[cntq].belr=(q[cntq].r-1)/block+1;
			q[cntq].id=i;
			q[cntq].num=mnum;
		}
		else {
			c[++cntm].x=read();
			c[cntm].val=read();
			c[cntm].pre=b[c[cntm].x];
			b[c[cntm].x]=c[cntm].val;
			mnum++;
		}
	}
	int l=1,r=0,ch=0;
	sort(q+1,q+cntq+1,cmp);
	for(int i=1;i<=cntq;i++) {
		while(l<q[i].l) del(a[l++]);
        while(l>q[i].l) add(a[--l]);
        while(r<q[i].r) add(a[++r]);
        while(r>q[i].r) del(a[r--]);
        while(ch>q[i].num) {
        	if(c[ch].x>=l&&c[ch].x<=r) {
        		del(c[ch].val);
        		add(c[ch].pre);
        	}
        	a[c[ch].x]=c[ch].pre;
        	ch--;
		}
		while(ch<q[i].num) {
			ch++;
			a[c[ch].x]=c[ch].val; 
			if(c[ch].x>=l&&c[ch].x<=r) {
        		del(c[ch].pre);
        		add(c[ch].val);
        	}
		}
        ans[q[i].id]=cur;
	}
	
	for(int i=1;i<=m;i++) {
		if(ans[i]!=-1) {
			printf("%lld\n",ans[i]);
		}
	}
	return 0;
}

樹上莫隊


免責聲明!

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



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