參考資料1
萬分感謝這個大佬,祝他報送清華北大!html
莫隊由莫濤神仙首次提出,是一種區間操做算法。git
即使是板子題,難度也很高(差評)算法
因此,在閱讀後文以前,請你先深呼吸,喝杯咖啡,吃點餅乾,聽聽本身喜歡的歌數組
而後,中止呼吸,放下杯子,扔開餅乾,摘下耳機,接受莫濤大神思想光輝的洗禮優化
先別談莫隊,咱們來回顧一下,遇到區間問題通常怎麼解決?spa
很好,暴力線段樹3d
也就是說,咱們一直在經過維護兩個序列——左序列\([l,mid]\)與右序列\([mid + 1,r]\),從而來維護\([l, r]\),固然,這個操做會一直遞歸下去指針
然而,當題目這麼問:code
令數組\(Q\)大小爲\(n\)且每一個元素\(Q_i < n\),有\(m\)個詢問,每次詢問給定\(l,r\),請找出\([l,r]\)中至少重複出現\(k\)此的數字的個數htm
換句話說:
在\(Q_l\)到\(Q_r\)內找出現次數多餘\(k\)的數字的個數
of course,你能夠暴力,但你會暴零
那麼咱們試着用線段樹,首先,你須要維護左邊的序列,而後你須要維護右邊的序列,而後……
而後你會發現很難作到短期甚至\(O(1)\)的時間完成對線段樹單一節點的維護,由於你老是要層層遞進向上疊加。
淦!這不是欺負人嗎
咱們先試試暴力吧,用個\(count\)記錄一下出現次數,而後在掃一遍
暴力是萬能的,答案固然正確,可是你的時間複雜度哭了——\(O(n^2)\)
那麼咱們能夠看看是否能夠改進一下,用上\(t(wo)p(oints)\)算法:
假設有兩個指針,\(l\)和\(r\),每次詢問的時候用移動\(l\)和\(r\)的方式來嘗試和要求區間重合
是否是有點蒙?我舉個栗子
此圖中,兩個Q是待求的區間
初始化\(r = 0,l = 1\)
此時,發現\(l\)和要求的區間左端重合了,而\(r\)沒有,那麼咱們把\(r\)往右邊移動一位
此時,\(r\)發現了一個新的值\(0\),總數記錄一下,繼續右移動
\(r\)又發現了一個新數值\(2\),總數記錄一下,繼續右移動
此處\(2\)被記錄過了,總數值不變
一直到\(r\)與右端點重合,獲得下圖:
第一個區間就算處理完了,咱們來看下一個
首先,\(l\)不在左端點,咱們把它右移
這一次,\(l\)所遇到的數值在區間\([l, r]\)只可以存在,總數不變
下一次也是如此,一直到
你會發現,這時,區間\([l,r]\)將(也就是在下一次移動後)不會有\(2\)存在了,那麼總數就一個\(-1\),而正好本題須要統計的就是區間內數值的個數,總數改變:
如此循環往復,獲得最終答案,因此咱們能夠得出這個代碼
int arr[maxn], cnt[maxn] // 每一個位置的數值、每一個數值的計數器 int l = 1, r = 0, now = 0; // 左指針、右指針、當前統計結果(總數) void add(int pos) { // 添加一個數 if(!cnt[arr[pos]]) ++ now; // 在區間中新出現,總數要+1 ++ cnt[arr[pos]]; } void del(int pos) { // 刪除一個數 -- cnt[arr[pos]]; if(!cnt[arr[pos]]) -- now; // 在區間中再也不出現,總數要-1 } void work() { for(int i = 1; i <= q; i ++) { int ql, qr; scanf("%d%d", &ql, &qr); while(l < ql) del(l++); // 左指針在查詢區間左方,左指針向右移直到與查詢區間左端點重合 while(l > ql) add(--l); // 左指針在查詢區間左端點右方,左指針左移 while(r < qr) add(++r); // 右指針在查詢區間右端點左方,右指針右移 while(r > qr) del(r--); // 不然左移 printf("%d\n", now); // 輸出統計結果 } }
嗯,幹得漂亮,可是這是莫隊嗎?不是
若是區間特別多,\(l,r\)反覆橫跳,結果皮斷了腿,時間複雜度\(O(nm)\)
那麼如今的問題已經變成了:如何儘可能減小\(l,r\)移動的次數?
首先,看到儘可能減小\(l,r\)移動的次數,咱們會想到排個序
排序排什麼的順序呢?是排端點嗎?顯然不是,哪怕左端點有序,右端點就會雜亂無章;右端點有序,左端點就會雜亂無章……
這裏,咱們運用一下分塊的思想,把序列分爲\(\sqrt{n}\)塊,把查詢區間按照左端點所在塊的序號排個序,若是左端點所在塊相同,再按右端點排序。
這個算法須要的時間複雜度爲\(sort+move_{\texttt{左指針}}\)
因爲\(sort\)的時間複雜度爲\(O(n\log n)\),\(move_{\texttt{作指針}}\)的時間複雜度爲\(O(n\sqrt{n})\),那麼總的時間複雜度爲\(O(n\sqrt{n})\)
好耶!降了一個根號!鼓掌!
其次,咱們須要考慮一下更新的策略
通常來講,咱們只要找到指針移動一位之後,統計數據與當前數據的差值,找出規律(能夠用數學方法或打表),而後每次移動時用這個規律更新就行
最後給出總代碼:
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> using namespace std; #define maxn 1010000 #define maxb 1010 int aa[maxn], cnt[maxn], belong[maxn]; int n, m, size, bnum, now, ans[maxn]; struct query { int l, r, id; } q[maxn]; int cmp(query a, query b) { return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r); } #define isdigit(x) ((x) >= '0' && (x) <= '9') int read() { int res = 0; char c = getchar(); while(!isdigit(c)) c = getchar(); while(isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar(); return res; } void printi(int x) { if(x / 10) printi(x / 10); putchar(x % 10 + '0'); } int main() { scanf("%d", &n); size = sqrt(n); bnum = ceil((double)n / size); for(int i = 1; i <= bnum; ++i) for(int j = (i - 1) * size + 1; j <= i * size; ++j) { belong[j] = i; } for(int i = 1; i <= n; ++i) aa[i] = read(); m = read(); for(int i = 1; i <= m; ++i) { q[i].l = read(), q[i].r = read(); q[i].id = i; } sort(q + 1, q + m + 1, cmp); int l = 1, r = 0; for(int i = 1; i <= m; ++i) { int ql = q[i].l, qr = q[i].r; while(l < ql) now -= !--cnt[aa[l++]]; while(l > ql) now += !cnt[aa[--l]]++; while(r < qr) now += !cnt[aa[++r]]++; while(r > qr) now -= !--cnt[aa[r--]]; ans[q[i].id] = now; } for(int i = 1; i <= m; ++i) printi(ans[i]),putchar('\n'); return 0; }
卡常數做爲OIer的屢見不鮮,相信你們必定不陌生了
卡常數包括:
而莫隊的神奇之處在於他的獨特優化:奇偶性排序
原代碼:
int cmp(query a, query b) { return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l]; }
改成
int cmp(query a, query b) { return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r); }
別人說跑的很快我還不信,本身跑了一下才知道……
真的跑的很快啊……
我知道,你拿着上面別個大佬寫的代碼(再次膜拜寫這個代碼的大佬orz)興沖沖的去刷題,一路上披荊斬棘,直到你看到了Luogu1903——國家集訓隊-數顏色,你完全傻了眼
媽耶,他要是這麼一修改我豈不是要從新sort?跑了跑了
因爲莫隊自己就是離線的,而你須要修改,得想個辦法讓他在線,具體作法是:「就是再弄一指針,在修改操做上跳來跳去,若是當前修改多了就改回來,改少了就改過去,直到次數恰當爲止。」 (再次感謝這個大佬,,好喜歡這個解釋)