莫隊學習筆記

轉載請帶上本博客地址:https://www.cnblogs.com/continue126/p/14450059.html
並註明原做者:@博客園:continue_1025,創做不易,請理解。html

普通莫隊

引入小例

\(zl\) 姐姐有一串數,因爲學生化太頭禿了,因此如今他想問你 \(m(m≤1e5)\) 次,其中 \(L\)\(R\) 區間出現次數在\(3\)次及以上的數有多少個?ios

解決方案

線段樹

效率低下,很差維護。算法

\(p.s.\)\(lmpp\) 巨佬說若是 \(3\) 次能夠取等的話,線段樹反而效率更高,巨佬們能夠本身嘗試,菜比這裏就不演示了)數組

故引入莫隊——一種處理區間問題的離線算法spa

0.算法名字的由來

莫隊算法,其中的「莫」指國家隊莫濤巨佬,CCCCOrz。code

1.基本原理

莫隊是優美的暴力htm

先讓咱們回到開頭來幫幫 \(zl\) 姐姐。blog

\(Continue\) 是個傻子,因此他打了個暴力;排序

for(int i=l;i<=r;i++)
{
	cnt[a[i]]++;
	if(cnt[a[i]]>=3)
		ans++;
}

若是每次詢問都這麼打的話,很明顯, \(O(nm)\) 的算法是會讓 \(zl\) 姐姐難堪的。(\(zl\) 姐姐:你來真的?get

聰明的你撿起了傻子 \(Continue\) 打的暴力,以爲好不容易打的,扔了多惋惜啊。

因而你開始對剛剛的暴力結果進行改造。

你想,既然咱們已經知道了 \([L,R]\) 的結果,那麼 \([L-1,R]\)\([L+1,R]\)\([L,R-1]\)\([L,R+1]\)的結果不就能夠也一塊兒很容易獲得了嗎?

\(O(1)\) 的時間裏,如今你的手裏如今已經有了 \(5\) 個答案。

這是多好的事,因而你將這個性質推廣到了全部的詢問。

詳細的,爲了方便,咱們不妨將推廣得來的四個答案一類稱做推廣區間,將推廣區間們對應的原區間 \([L,R]\) 稱做原區間。只要咱們知道了原區間的答案,那麼要求的推廣區間便也就可求了。

因此如今問題就轉化爲了:「如何使詢問區間成爲一個推廣區間」。進一步地,因爲咱們沒法改變詢問,這個問題變成了「如何使推廣區間匹配上詢問區間」

顯然,咱們能夠經過不斷修改原區間的方式,來匹配與詢問區間一致的推廣區間

很明顯,這種不斷變化範圍的操做,咱們能夠經過 \(while\) 循環實現。

但是若是每次都 \(while\) ,咱們的代碼仍然是一份傻子代碼——\(T\) 得慘不忍睹\(g2020\) \(lvt\) \(&&\) \(dlz\)大佬語)

因此接下來纔是真正應用時的莫隊:分塊+\(sort\)

2.基本莫隊

有了上面的一些推論,如今你意識到,每次詢問時都要根據查詢區間的大小調整原區間大小,且因爲詢問區間並不相同(不然該問題將沒有意義),因此這個操做是必然的

在必然的狀況下,咱們要儘量的使該操做盡可能的快,由此才能作到優美的暴力。

再次分析上面的過程,咱們發現該操做的主要時耗來源於鎖定所需區間的過程,因此咱們應儘量的將每次須要的推廣區間之間的差減少,以此來減小變化區間範圍的次數,提升了效率。

而達到此目的的惟一方式就是對查詢區間進行排序

這即是優美莫隊裏面的\(sort\)部分

至於排序的標準,天然要依靠於分塊啦~

因爲咱們要求兩個區間儘可能的類似,因此應知足單調性,否則會浪費時間。

如圖。

圖一

圖二

注:紫色曲線表明每次鎖定區間時需移動的長度。

圖一是未排序的效果,能夠看見陰影的部分咱們是重複移動了的,這樣十分浪費時間。

只要排序成圖二這樣,要移動的區間就不再會重疊啦~

肯定了排序的任務,那麼排序的關鍵字呢?

答案是分塊

分塊合理地將節點劃分了不一樣的區間,這樣就能夠較快的比較。

咱們經過左端點所處的塊進行排序,若處於同一個塊則比較右端點。這樣就能夠科學有效的下降時間啦~

3.代碼

上面的都懂了,接下來就是一份普通莫隊的模板代碼,通常的題均可以變着花樣套板子。(固然不能算帶權莫隊樹上莫隊)

\(Problem\):HH的項鍊

這道題 \(luogu\) 是卡了莫隊的(但仍是有神仙巨佬卡過去了),正解是樹狀數組。故在這裏只是做爲練手題。A6個點,T4個點就差很少了。主要是思想,思想!

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>

using namespace std;

const int maxn=2e6+10;
const int inf=1<<30;

inline int Read()
{
	int s=0;
	int f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-')
			f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		s=(s<<1)+(s<<3)+ch-'0';
		ch=getchar();
	}
	return s*f;
}

int n,m;
struct Num
{
	int l,r; //詢問的區間
	int num; //詢問的答案
	int id; //詢問的次序
}a[maxn];

int temp[maxn];

int bel[maxn]; //belong

bool cmp(Num x,Num y)
{
	return ((bel[x.l]==bel[y.l]) && (x.r<y.r)) || (bel[x.l]<bel[y.l]);
}

bool cmp2(Num x,Num y)
{
	return x.id<y.id;
}

int cnt[maxn]; //記錄次數的數組
int top;

int ans; //答案有幾個

inline void Add(int x)
{
	cnt[x]++;
	if(cnt[x]==1)
		ans++;
}

inline void Dele(int x)
{
	cnt[x]--;
	if(!cnt[x])
		ans--;
}

int main()
{
	n=Read();
	
	int k=sqrt(n); //分塊
	
	for(int i=1;i<=n;i++)
	{
		temp[i]=Read();
		bel[i]=(i-1)/k+1;
	}

	m=Read();
	for(int i=1;i<=m;i++)
	{
		int x,y;
		x=Read();
		y=Read();
		if(x>y)
			swap(x,y);
		a[i].l=x;
		a[i].r=y;
		a[i].id=i;
	}
	
	sort(a+1,a+m+1,cmp);
	
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		int x=a[i].l;
		int y=a[i].r;
		
                //鎖定區間的過程
		while(l>x)
			Add(temp[l-1]),--l;
		while(l<x)
			Dele(temp[l]),++l;
		while(r<y)
			Add(temp[r+1]),++r;			
		while(r>y)
			Dele(temp[r]),--r;

		a[i].num=ans;
	}
	
	sort(a+1,a+m+1,cmp2); //離線算法按原序輸出答案
	
	for(int i=1;i<=m;i++)
		printf("%d\n",a[i].num);
	return 0;
}

\(zl\) 姐姐感激的眼神鼓舞下,更進一步吧!


帶權莫隊

引入小例

\(zl\) 姐姐有一串數,因爲他太可愛了,因此如今原基礎上增長一個操做:將第 \(k\) 個數變成 \(num\)

解決方案

因爲如今的問題仍然保留詢問操做,因此咱們仍考慮使用莫隊解決。

因爲修改數值的操做,咱們須要莫隊能夠儲存數據。由此咱們引入一個新的結構:帶權莫隊

1.基本原理

帶權莫隊的基本原理和普通莫隊是同樣的。

只會打暴力的傻子 \(Continue\) 是這麼想的:
只要每次一輸入和當前區間有關的修改,就立刻暴力修改

很明顯,這會 \(TLE\) ,由於區間可能不定。

這很像最開始咱們處理基礎莫隊時遇到的問題。那麼此次一樣的,咱們使用 \(sort\) 來解決。

咱們新增一個關鍵字 \(tim\) \((time)\) ,其中記錄了對當前詢問有影響的修改操做的序號\(sort\) 的時候將 \(tim\) 做爲第三關鍵字,這樣既能保證基礎莫隊對詢問操做處理的正確性,又能及時處理修改操做。由於修改操做複雜度低,因此這樣就不會 \(TLE\) 了。

修改操做單獨建一個結構體, \(perfect\)

3.代碼

\(Problem\)數顏色

這道題題解裏面有一個巨佬,在修改操做的時候很神仙的運用了轉換的思想,個人代碼寫得差多了,因此傳送門就放在這裏,你們能夠去看。

這題仍是卡莫隊,因此分塊的塊數設置爲常數就不會卡了。

不少大佬都是將分塊的塊數 \(k\) 設置爲的 (啊沒錯這個圖是複製的),由於這樣最快。其具體證實戳這位大佬的題解,菜比我證不來也看不懂(

完。

鳴謝

感謝 \(zl\) 小姐姐不知不覺間提供給個人精神支持。

感謝我本身是個大菜比。

相關文章
相關標籤/搜索