一.什麼是莫隊算法?
莫隊算法是用來處理一類無修改的離線區間詢問問題。——(摘自前國家隊隊長莫濤在知乎上對莫隊算法的解釋。)html
莫隊算法是前國家隊隊長莫濤在比賽的時候想出來的算法。ios
傳說中能解決一切區間處理問題的莫隊算法。git
準確的說,是離線區間問題。可是如今的莫隊被拓展到了有樹上莫隊,帶修莫隊(即帶修改的莫隊)。這裏只先講普通的莫隊。算法
還有一點,重要的事情說三遍!莫隊不是提莫隊長!莫隊不是提莫隊長!!莫隊不是提莫隊長!!!數組
二.爲何要使用莫隊算法?
看一個例題:給定一個n(n<50000)元素序列,有m(m<200000)個離線查詢。每次查詢一個區間L~R,問每一個元素出現次數爲k的有幾個。(必須剛好是k,不能大於也不能小於)優化
這時候dalao們就直接樹狀數組線段樹主席樹拍上去了,身爲蒟蒻的我躲在角落瑟瑟發抖。該怎麼辦?spa
這時候就要使用莫隊算法了。.net
三.莫隊算法的思想?
接着上面的例題,直接暴力怎麼樣??設計
確定會T的啊。指針
可是若是這個暴力咱們給優化一下呢?
咱們想,有兩個指針 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$仍是$T$...
怎麼辦?
可是這其實就是莫隊算法的核心了。咱們的莫隊算法還有優化。
這就是莫隊算法最精明的地方(我認爲的qwq),也正是有了這個優化,莫隊算法被稱爲:優雅的暴力。
咱們想,由於每次查詢是離線的,因此咱們先給每次的查詢排一個序。
一種直觀的辦法是按照左端點排序,再按照右端點排序。可是這樣的表現很差。特別是面對精心設計的數據,這樣方法表現得不好。
舉個栗子,有6個詢問以下:(1, 100), (2, 2), (3, 99), (4, 4), (5, 102), (6, 7)。
這個數據已經按照左端點排序了。用上述方法處理時,左端點會移動6次,右端點會移動移動98+97+95+98+95=483次。
其實咱們稍微改變一下詢問處理的順序就能作得更好:(2, 2), (4, 4), (6, 7), (5, 102), (3, 99), (1, 100)。
左端點移動次數爲2+2+1+2+2=9次,比原來稍多。右端點移動次數爲2+3+95+3+1=104,右端點的移動次數大大下降了。
上面的過程啓發咱們:
咱們不該該嚴格按照升序排序,而是根據須要靈活一點的排序方法
那麼排序的方法是
分塊。
咱們把全部的元素分紅多個塊。再按照左端點塊編號從小到大排序,左端點塊編號相同按右端點塊編號從小到大。
這樣對於不一樣的查詢
例如:
咱們有長度爲9的序列。
1 2 3 4 5 6 7 8 9 分爲1——3 4——6 7——9
查詢有7組。[1 2] [2 9] [1 3] [6 9] [5 8] [3 8] [8 9]
排序後就是:[1 2] [1 3] [3 8] [2 9] | [5 8] [6 9] | [8 9]
而後咱們按照這個順序移動指針就好啦~
時間複雜度證實
其實從不一樣的角度看,證法不少:對於左端點在一個塊中時,右端點最壞狀況是從儘可能左到儘可能右,因此右端點跳時間複雜度O(n),左端點一共能夠在n^0.5個塊中,因此總時間複雜度O(n*n^0.5) = O(n^1.5)。
四.具體代碼實現:
1.對於每組查詢的記錄和排序:
l,r爲左右區間編號,p是第幾組查詢的編號
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; }
2.處理和初始變量:
answer就是所求答案,bl是分塊數量,a[]是原序列,ans[]是記錄原查詢序列下的答案,cnt[]是記錄對於每一個數i,cnt[i]表示i出現過的次數,curL和curR再也不解釋,nmk題意要求。
int answer, a[maxn], m, n, bl, ans[maxn], cnt[maxn], k, curL = 1, curR = 0; void add(int pos)//添加 { //do sth... } void remove(int pos)//去除 { //do sth... } //通常寫法都是邊處理 邊根據處理求答案。cnt[a[pos]]就是在pos位置上原序列a出現的次數。
3.主體部分及輸出:
預處理查詢編號,用四個while移動指針順便處理。
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;
在這裏着重說下四個while
當curL < L 時,咱們當前curL是已經處理好的了。因此remove時先去除當前curL再++
當curL > L 時,咱們當前curL是已經處理好的了。因此 add 時先--再加上改後curL
當curR > R 時,咱們當前curR是已經處理好的了。因此remove時先去除當前curR再--
當curR < R 時,咱們當前curR是已經處理好的了。因此 add 時先++再加上改後curR
五.一些例題
【luogu P2709 小B的詢問】
$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 P4462 [CQOI2018]異或序列】
ax+ax-1+...+ay = cntx+cnty 這樣把一段序列變成兩段相加跑莫隊。
#include <cstdio> #include <algorithm> #include <iostream> #include <cmath> using namespace std; const int maxn = 200010; int curR = 0, curL = 1, answer,a[maxn], ans[maxn], cnt[maxn], n, m, k, bl; struct query{ int l,r,p; }q[maxn]; bool cmp(const query &a, const query &b) { return (a.l/bl) == (b.l/bl) ? a.r<b.r : a.l<b.l; } inline void add(int pos) { cnt[a[pos]]++; answer+=cnt[a[pos]^k]; } inline void remove(int pos) { cnt[a[pos]]--; answer-=cnt[a[pos]^k]; } int main() { scanf("%d%d%d",&n,&m,&k); bl = sqrt(n); cnt[0] = 1; for(int i = 1; i <= n; i++) { scanf("%d",&a[i]); a[i] ^= a[i-1]; } for(int i = 1; i <= m; i++) { scanf("%d%d",&q[i].l,&q[i].r); q[i].p = i; } sort(q+1,q+1+m,cmp); for(int i = 1; i <= m; i++) { while(curL < q[i].l) remove(curL-1),curL++; while(curL > q[i].l) curL--,add(curL-1); while(curR < q[i].r) add(++curR); while(curR > q[i].r) remove(curR--); ans[q[i].p] = answer; } for(int i = 1; i <= m; i++) printf("%d\n",ans[i]); return 0; }
六.總結
莫隊算法適用條件是比較苛刻的嗎?是的。 ①題目必須離線
②可以以極少的時間推出旁邊區間(通常是O(1))
③沒有修改或者修改不太苛刻
④基於分塊,分塊不行,它也好不了哪裏去
但莫隊的思想美妙,代碼優美,你值得擁有。莫隊的排序思想也爲衆多離線處理的題目提供了完整的思路。
咱們能夠看出來,對於莫隊這種模擬式的暴力算法很好理解。 也比較實用,亂搞神器不是浪得虛名。
莫隊是我自學的,因此在文章裏或許會有些我的理解上的誤差,還請各位dalao能賜教。自學能力是須要培養、鍛鍊的。
其次是也引用了一些dalao寫的很好的文章,也是我自學時用到的資料:
這是幾篇我學莫隊時參考的博客,若是以爲我講的不夠詳細,能夠借鑑。
祝各位OI路途能越走越順!
本蒟蒻$QQ$ 935145183/3203600070