用了大約1h搞定了基礎的莫隊算法。寫篇博客算是檢驗下自己的學習成果。
一.什么是莫隊算法?
莫隊算法是用來處理一類無修改的離線區間詢問問題。——(摘自前國家隊隊長莫濤在知乎上對莫隊算法的解釋。)
莫隊算法是前國家隊隊長莫濤在比賽的時候想出來的算法。
傳說中能解決一切區間處理問題的莫隊算法。
准確的說,是離線區間問題。但是現在的莫隊被拓展到了有樹上莫隊,帶修莫隊(即帶修改的莫隊)。這里只先講普通的莫隊。
還有一點,重要的事情說三遍!莫隊不是提莫隊長!莫隊不是提莫隊長!!莫隊不是提莫隊長!!!
二.為什么要使用莫隊算法?
看一個例題:給定一個n(n<50000)元素序列,有m(m<200000)個離線查詢。每次查詢一個區間L~R,問每個元素出現次數為k的有幾個。(必須恰好是k,不能大於也不能小於)
我們很容易想到用線段樹或者樹狀數組直接做,但是我們想,如果是用線段樹或者樹狀數組做而且我們不會優化的話(請dalao無視掉,您可以直接線段樹做了。)每次修改和維護會很麻煩,線
段樹和樹狀數組的優勢體現不出來。
這時候就要使用莫隊算法了。
三.莫隊算法的思想怎么理解?
接着上面的例題,直接暴力怎么樣??
肯定會T的啊。(luogu P1972 [SDOI2009]HH的項鏈 原數據居然可以mn模擬過......當然現在不行了)
但是如果這個暴力我們給優化一下呢?
我們想,有兩個指針curL和curR,curL指向L,curR指向R。
L和R是一個區間的左右兩端點。
利用cnt[]記錄每個數出現的次數,每次只是cnt[a[curL]] cnt[a[curR]]修改。
舉個栗子:
我們現在處理了curL—curR區間內的數據,現在左右移動,比如curL到curL-1,只需要更新上一個新的3,即curL-1。
那么curL到curL+1,我們只需要去除掉當前curL的值。因為curL+1是已經維護好了的。
curR同理,但是要注意方向哦!curR到curR+1是更新,curR到cur-1是去除。
我們先計算一個區間[curL curR]的answer,這樣的話,我們就可以用O(1)轉移到[curL-1 curR] [curL+1 curR] [curL curR+1] [curL curR-1]上來並且求出這些區間的answer。
我們利用curL和curR,就可以移動到我們所需要求的[L R]上啦~
這樣做會快很多,但是......
如果有個**數據,讓你在每個L和R間來回跑,而且跨度很大呢??
我們每次只動一步,豈不是又T了??
但是這其實就是莫隊算法的核心了。我們的莫隊算法還有優化。
這就是莫隊算法最精明的地方(我認為的qwq),也正是有了這個優化,莫隊算法被稱為:優雅的暴力
我們想,因為每次查詢是離線的,所以我們先給每次的查詢排一個序。
排序的方法是分塊。
我們把所有的元素分成多個塊(即分塊)。分了塊跑的會更快。再按照右端點從小到大,左端點塊編號相同按右端點從小到大。
這樣對於不同的查詢
例如:
我們有長度為9的序列。
1 2 3 4 5 6 7 8 9 分為1——3 4——6 7——9
查詢有7組。[1 2] [2 1000] [1 3] [6 9] [5 8] [3 8] [8 9]
排序后就是:[1 2] [1 3] [3 8] [2 1000] | [5 8] [6 9] | [8 9]
然后我們按照這個順序移動指針就好啦~
這樣,不斷地移動端點指針+精妙的排序,就是普通莫隊的思想啦~
時間復雜度證明
關於時間復雜度的證明:給一個角度,其實從不同的角度看,證法很多: 對於左端點在一個塊中時,右端點最壞情況是從盡量左到盡量右,所以右端點跳時間復雜度O(n),左端點一共可以在n0.5個塊中,所以總時間復雜度O(n*n0.5) = (n1.5)。
四.具體代碼實現:
1.對於每組查詢的記錄和排序:
l,r為左右區間編號,p是第幾組查詢的編號
1 struct query{ 2 int l, r, p; 3 }e[maxn]; 4 5 bool cmp(query a, query b) 6 { 7 return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l < b.l; 8 }
2.處理和初始變量:
answer就是所求答案,bl是分塊數量,a[]是原序列,ans[]是記錄原查詢序列下的答案,cnt[]是記錄對於每個數i,cnt[i]表示i出現過的次數,curL和curR不再解釋,nmk題意要求。
1 int answer, a[maxn], m, n, bl, ans[maxn], cnt[maxn], k, curL = 1, curR = 0; 2 void add(int pos)//添加 3 { 4 //do sth... 5 } 6 void remove(int pos)//去除 7 { 8 //do sth... 9 } 10 //一般寫法都是邊處理 邊根據處理求答案。cnt[a[pos]]就是在pos位置上原序列a出現的次數。
3.主體部分及輸出:
預處理查詢編號,用四個while移動指針順便處理。
在這里着重說下四個while
我們設想有一條數軸:
當curL < L 時,我們當前curL是已經處理好的了。所以remove時先去除當前curL再++
當curL > L 時,我們當前curL是已經處理好的了。所以 add 時先--再加上改后curL
當curR > R 時,我們當前curR是已經處理好的了。所以remove時先去除當前curR再--
當curR < R 時,我們當前curR是已經處理好的了。所以 add 時先++再加上改后curR
1 n = read(); m = read(); k = read(); 2 bl = sqrt(n); 3 4 for(int i = 1; i <= n; i++) 5 a[i] = read(); 6 7 for(int i = 1; i <= m; i++) 8 { 9 e[i].l = read(); e[i].r = read(); 10 e[i].p = i; 11 } 12 13 sort(e+1,e+1+m,cmp); 14 15 for(int i = 1; i <= m; i++) 16 { 17 int L = e[i].l, R = e[i].r; 18 while(curL < L) 19 remove(curL++); 20 while(curL > L) 21 add(--curL); 22 while(curR > R) 23 remove(curR--); 24 while(curR < R) 25 add(++curR); 26 ans[e[i].p] = answer; 27 } 28 for(int i = 1; i <= m; i++) 29 printf("%d\n",ans[i]); 30 return 0;
五.實戰莫隊:
【luogu P1972 [SDOI2009]HH的項鏈】
https://www.luogu.org/problemnew/show/P1972
因為原來數據被大模擬過了,所以數組50000要多開。add和remove根據不同情況處理,如果當前有相同的了再++肯定不是1,如果當前相同的不止一個,remove--的時候肯定不是0,不會造成影響。反之則可以判斷有多少是不同元素。
1 //HH的項鏈 2 #include <cstdio> 3 #include <algorithm> 4 #include <iostream> 5 #include <cmath> 6 using namespace std; 7 const int maxn = 200001; 8 const int maxm = 500001; 9 int m, n, bl, answer, curL = 1, curR = 0, ans[maxn], a[maxn], cnt[maxm];//a是原序列 cnt是記錄每個數字出現的次數 10 inline int read() 11 { 12 int k=0; 13 char c; 14 c=getchar(); 15 while(!isdigit(c))c=getchar(); 16 while(isdigit(c)){k=(k<<3)+(k<<1)+c-'0';c=getchar();} 17 return k; 18 } 19 struct query{ 20 int l, r, p;//l 左區間 r 右區間 p 位置的編號 21 /*friend bool operator < ( query a, query b ) { 22 return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l<b.l ; 23 }*/ 24 }e[maxn]; 25 bool cmp(query a, query b) 26 { 27 return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l<b.l; 28 } 29 void add(int pos) 30 { 31 if((++cnt[a[pos]]) == 1) ++answer; 32 } 33 void remove(int pos) 34 { 35 if((--cnt[a[pos]]) == 0) --answer; 36 } 37 int main() 38 { 39 n = read(); 40 for(int i = 1; i <= n; i++) 41 a[i] = read(); 42 43 m = read(); 44 45 bl = sqrt(n); 46 47 for(int i = 1; i <= m; i++) 48 { 49 e[i].l = read(); e[i].r = read(); 50 e[i].p = i; 51 } 52 sort(e+1,e+1+m,cmp); 53 54 for(int i = 1; i <= m; i++) 55 { 56 int L = e[i].l, R = e[i].r; 57 while(curL < L) 58 remove(curL++); 59 while(curL > L) 60 add(--curL); 61 while(curR > R) 62 remove(curR--); 63 while(curR < R) 64 add(++curR); 65 ans[e[i].p] = answer; 66 } 67 for(int i = 1; i <= m; i++) 68 printf("%d\n",ans[i]); 69 return 0; 70 }
【luogu P2709 小B的詢問】
https://www.luogu.org/problemnew/show/P2709#sub
add和remove對於平方相加減的運算利用完全平方式逆回去。
1^2 = 1;
2^2 = (1+1)^2 = 1 + 1*2 + 1;
3^2 = (1+2)^2 = 1 + 2*2 + 4;
4^2 = (1+3)^2 = 1 + 3*2 + 9;
......
//小B的詢問 #include <cstdio> #include <algorithm> #include <iostream> #include <cmath> using namespace std; const int maxn = 50001; int answer, a[maxn], m, n, bl, ans[maxn], cnt[maxn], k, curL = 1, curR = 0; void add(int pos) { answer+=(((cnt[a[pos]]++)<<1)+1);//完全平方式展開 } void remove(int pos) { answer-=(((--cnt[a[pos]])<<1)+1);//完全平方式展開 } inline int read() { int k=0; char c; c=getchar(); while(!isdigit(c))c=getchar(); while(isdigit(c)){k=(k<<3)+(k<<1)+c-'0';c=getchar();} return k; } struct query{ int l, r, p; }e[maxn]; bool cmp(query a, query b) { return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l < b.l; } int main() { n = read(); m = read(); k = read(); bl = sqrt(n); for(int i = 1; i <= n; i++) a[i] = read(); for(int i = 1; i <= m; i++) { e[i].l = read(); e[i].r = read(); e[i].p = i; } sort(e+1,e+1+m,cmp); for(int i = 1; i <= m; i++) { int L = e[i].l, R = e[i].r; while(curL < L) remove(curL++); while(curL > L) add(--curL); while(curR > R) remove(curR--); while(curR < R) add(++curR); ans[e[i].p] = answer; } for(int i = 1; i <= m; i++) printf("%d\n",ans[i]); return 0; }
這兩個題我都用了快讀在里面。可以摘下來當板子背。
最后!我要吐槽一句!!luogu試煉場線段樹和樹狀數組的題!我線段樹一個也過不了!(我真是太蒟蒻了)所以還是莫隊大法好!
這是幾篇我學莫隊時參考的博客,如果覺得我講的不夠詳細,可以借鑒。
https://blog.csdn.net/wzw1376124061/article/details/67640410