莫隊の學習筆記
0x10 莫隊演算法的概念
莫隊演算法是由莫濤提出的演算法。莫隊演算法可以解決一類離線區間詢問問題,適用性極為廣泛。同時將其加以擴充套件,便能輕鬆處理樹上路徑詢問以及支援修改操作。 ——摘自 \(Oi-wiki\)。
0x20 普通莫隊演算法
-
0x21 形式
給定一個序列 \(Q\) ,我們設 \(n\) 和 \(m\) 同階。對於序列上的查詢問題,給定查詢區間 \([L_i,R_i]\),我們可以以 \(O(1)\) 的速度擴充套件答案到與查詢區間相鄰的四個區間,即:\([L_{i-1}, R]\), \([L_{i+1}, R]\) ,\([L_i, R_{i+1}]\) 和 \([L_i,R_{i-1}]\)
程式碼模板如下:
for (int i = 1; i <= m; i ++) { while (r < mt[i].r) add (++ r); while (l > mt[i].l) add (-- l); while (l < mt[i].l) del (l ++); while (r > mt[i].r) del (r --); ans[mt[i].id] = res; }
-
0x22 實現
對於 \(m\) 個詢問,我們將其先儲存起來,然後再排序,最後離線處理這些資料。
-
0x23 排序
正常來說,我們的排序方法都是以 \([L_i, R_i]\) 所在的塊的編號為第一關鍵字,以右端點為第二關鍵字從小到大排序。
程式碼實現如下:
inline bool cmp (MOTEAM x, MOTEAM y) { if (block[x.l] == block[y.l]) { //x和y的左端點都在同一個塊中時,按右端點進行排序。 return x.r < y.r; } if (block[x.l] != block[y.l]) { //x和y的左端點不在同一個塊中時,按左端點進行排序。 return x.l < y.l; } }
-
0x24 優化
在很多時候,我們會盲目的將塊長設定為 \(\sqrt{n}\),所以如果遇到 \(m\) 和 \(\sqrt{n}\) 同階時,而恰好我們將塊長設定為 \(\sqrt{n}\) 時,這樣我們就有可能被構造出的資料卡掉。
那我們應該將塊長設為什麼呢?在隨機資料下,設為 \(\frac{n}{\sqrt{\frac{2}{3} m}}\) 會快一些,大概能優化 \(\frac{1}{10}\) 左右。證明留給讀者自行探索。
我們發現,莫隊演算法實際上看上去僅僅是暴力的優化。但是為什麼使用這種"看似暴力"的做法卻可以通過高難度資料結構題目呢?很顯然,我們的演算法複雜度在離線進行排序的時候被優化了。所以,這一排序節省了我們很多的時間。
於是又有神仙創造出了一種新的排序方法:奇偶性排序。
首先,我們原來的排序方法,雖然讓右端點的大跳動減少了很多。但是,右端點的跳動依舊存在,使我們的時間複雜度大幅度升高。我們能否創造一種方法排序使其更優化呢?
我們在選擇奇數塊時,按照右端點從小到大排序,在偶數塊時從大到小排序,這樣可以減少右端點進行大跳動的次數。
而為什麼這樣排序會快呢?是因為右指標移動到右邊後就不需要跳回左邊了,而跳回左邊後處理下一個塊是要跳動到右邊的。很明顯,這樣就會優化近一半的時間複雜度。
程式碼實現如下:
inline bool cmp (MOTEAM x, MOTEAM y) { return block[x.l] == block[y.l] ? (block[y.l] & 1) ? x.r < y.r : x.r > y.r : x.l < y.l }
-
0x25 例題 \(1\)
很經典的一道題。這道題求 \([L_i,R_i]\) 區間只出現過一次的數字個數。這樣我們的 \(add\) 和 \(del\) 函式就很好設計。
inline void add (int x) { cnt[a[x]] ++; if (cnt[a[x]] == 1) { res ++; } } inline void del (int x) { cnt[a[x]] --; if (cnt[a[x]] == 0) { res --; } }
但是這道題的出題人也許特意卡莫隊做法,正常寫法應該是 \(65pts\) ,會 \(TLE\) \(5\)個點。
所以我們需要大力卡常!我們運用上面的優化技巧,修改塊長,使用奇偶性排序方法,將 \(add\) 和 \(del\) 改成位運算,提前預處理左端點所在塊,吸口氧就可以通過該題了。
-
0x26 例題 \(2\)
這道題和上道題幾乎相同,我們只需要用正常的莫隊做法就可以通過。
核心程式碼如下:
inline void add(int x, int &res) { if (num[a[x]] == 0) { //次數未增加前該數次數出現次數為0,就沒有出現過,即不重複。 res ++; } num[a[x]] ++; } inline void del(int x, int &res) { num[a[x]] --; if (num[a[x]] == 0) { //次數減少後該數次數出現次數為0,就出現過,個數減少。 res --; } }
-
0x27 例題 \(3\)
我們看到題目,每個數字貢獻為出現次數的平方,這樣我們的 \(add\) 和 \(del\) 函式就不太好編寫。
這樣我們就需要用到完全平方的小知識解決此題。
即:
\((a+1)^2 = a^2+1+2a\)
於是我們的 \(add\) 函式和 \(del\) 函式有這種寫法:
inline void add (int x) { ans += cnt[a[x]] * 2 + 1; cnt[a[x]] ++; } inline void del (int x) { ans -= cnt[a[x]] * 2 - 1; cnt[a[x]] --; }
但是不要忘記,莫隊是暴力美學,當然我們的 \(add\) 函式和 \(del\) 函式也可以暴力模擬。
核心程式碼如下:
inline void add (int x) { memo -= cnt[a[x]] * cnt[a[x]]; cnt[a[x]] ++; memo += block[a[x]] * block[a[x]]; } inline void del (int x) { memo -= cnt[a[x]] * cnt[a[x]]; cnt[a[x]] --; memo += cnt[a[x]] * cnt[a[x]]; }
-
0x28 例題 \(4\)