1. 程式人生 > 其它 >莫隊の學習筆記

莫隊の學習筆記

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}]\)

    。 這樣,我們實現的時間複雜度就是 \(O(n×\)塊長\()\)。而我們通常設的塊長為 \(\sqrt{n}\) ,那麼我們的時間複雜度就是 \(O(n\sqrt{n})\),這對於我們 \(n\) 達到 \(10^5\) 時,是一個非常快速的做法,且程式碼實現較易。

    程式碼模板如下:

    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\)

    P1972 [SDOI2009]HH的項鍊

    很經典的一道題。這道題求 \([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\) 改成位運算,提前預處理左端點所在塊,吸口氧就可以通過該題了。

    AC CODE


  • 0x26 例題 \(2\)

    SP3267 DQUERY - D-query

    這道題和上道題幾乎相同,我們只需要用正常的莫隊做法就可以通過。

    核心程式碼如下:

    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\)

    P2709 小B的詢問

    我們看到題目,每個數字貢獻為出現次數的平方,這樣我們的 \(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\)

P3901 數列找不同