根號分治莫隊

violet-su發表於2024-08-14

莫隊

參考文章:
莫隊細講——從零開始學莫隊
莫隊演算法——從入門到黑題
oiwiki--普通莫隊

莫隊簡介

莫隊演算法是由莫濤提出的演算法。在莫濤提出莫隊演算法之前,莫隊演算法已經Codeforces 的高手圈裡小範圍流傳,但是莫濤是第一個對莫隊演算法進行詳細歸納總結的人。莫濤提出莫隊演算法時,只分析了普通莫隊演算法,但是經過 OIer 和 ACMer 的集體智慧改造,莫隊有了多種擴充套件版本。
莫隊演算法可以解決一類離線區間詢問問題,適用性極為廣泛。同時將其加以擴充套件,便能輕鬆處理樹上路徑詢問以及支援修改操作。

普通莫隊演算法

形式

假設 \(n = m\),那麼對於序列上的區間詢問問題,如果從 \([l, r]\) 的答案能夠 \(O(1)\) 擴充套件到 \([l-1, r]\), \([l+1, r]\), \([l, r+1]\), \([l, r-1]\)(即與 \([l, r]\) 相鄰的區間)的答案,那麼可以在 \(O(n\sqrt{n})\) 的複雜度內求出所有詢問的答案。

解釋

離線後排序,順序處理每個詢問,暴力從上一個區間的答案轉移到下一個區間答案(一步一步移動即可)。

排序方法

對於區間 \([l, r]\), 以 \(l\) 所在塊的編號為第一關鍵字,\(r\) 為第二關鍵字從小到大排序。

略.......
直接上題目
P1494 [國家集訓隊] 小 Z 的襪子

[國家集訓隊] 小 Z 的襪子

題目描述

upd on 2020.6.10 :更新了時限。

作為一個生活散漫的人,小 Z 每天早上都要耗費很久從一堆五顏六色的襪子中找出一雙來穿。終於有一天,小 Z 再也無法忍受這惱人的找襪子過程,於是他決定聽天由命……

具體來說,小 Z 把這 \(N\) 只襪子從 \(1\)\(N\) 編號,然後從編號 \(L\)\(R\) 的襪子中隨機選出兩隻來穿。儘管小 Z 並不在意兩隻襪子是不是完整的一雙,他卻很在意襪子的顏色,畢竟穿兩隻不同色的襪子會很尷尬。

你的任務便是告訴小 Z,他有多大的機率抽到兩隻顏色相同的襪子。當然,小 Z 希望這個機率儘量高,所以他可能會詢問多個 \((L,R)\) 以方便自己選擇。

然而資料中有 \(L=R\) 的情況,請特判這種情況,輸出0/1

輸入格式

輸入檔案第一行包含兩個正整數 \(N\)\(M\)\(N\) 為襪子的數量,\(M\) 為小 Z 所提的詢問的數量。接下來一行包含 \(N\) 個正整數 \(C_i\),其中 \(C_i\) 表示第 \(i\) 只襪子的顏色,相同的顏色用相同的數字表示。再接下來 \(M\) 行,每行兩個正整數 \(L, R\) 表示一個詢問。

輸出格式

包含 \(M\) 行,對於每個詢問在一行中輸出分數 \(A/B\) 表示從該詢問的區間 \([L,R]\) 中隨機抽出兩隻襪子顏色相同的機率。若該機率為 \(0\) 則輸出 0/1,否則輸出的 \(A/B\) 必須為最簡分數。(詳見樣例)

樣例 #1

樣例輸入 #1

6 4
1 2 3 3 3 2
2 6
1 3
3 5
1 6

樣例輸出 #1

2/5
0/1
1/1
4/15

提示

\(30\%\) 的資料中,\(N,M\leq 5000\)

\(60\%\) 的資料中,\(N,M \leq 25000\)

\(100\%\) 的資料中,\(N,M \leq 50000\)\(1 \leq L \leq R \leq N\)\(C_i \leq N\)

AC程式碼:

#include<bits/stdc++.h>
#define int long long
#define debug() cout<<"----------debug-------------"
using namespace std;

const int N = 500010; // 定義陣列最大大小
int n, q; // n 是陣列大小,q 是查詢數量
int c[N]; // 儲存輸入陣列
array<int, 3> que[N]; // 儲存查詢的陣列,每個查詢是一個三元組 {l, r, i}
int B = 750; // 分塊的大小
int ans[N]; // 儲存每個查詢的結果
int tmp = 0; // 臨時變數,用於計算當前區間內不同元素對的個數
int ans2[N]; // 記錄每個查詢的分母
int cnt[N]; // 記錄每個數出現的次數

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); // 加速輸入輸出

    cin >> n >> q; // 讀取陣列大小和查詢數量
    for (int i = 1; i <= n; i++)
        cin >> c[i]; // 讀取陣列元素

    for (int i = 0; i < q; i++) {
        int l, r;
        cin >> l >> r; // 讀取每個查詢的區間 [l, r]
        que[i] = {l, r, i}; // 儲存查詢
        ans2[i] = (r - l) * (r - l + 1) / 2; // 計算分母
    }

    // 按照莫隊演算法的分塊策略對查詢進行排序
    sort(que, que + q, [&](array<int, 3> a, array<int, 3> b) {
        int d = a[0] / B;
        if (a[0] / B != b[0] / B) return a[0] / B < b[0] / B;
        //if (d % 2 == 1) return a[1] < b[1];
        //else return a[1] > b[1];
        return a[1]<b[1];
    });

    int l = 1, r = 0; // 初始化指標 l 和 r

    // 新增元素時更新 tmp 和 cnt
    auto add = [&](int x) {
        tmp += cnt[c[x]];
        cnt[c[x]]++;
    };

    // 刪除元素時更新 tmp 和 cnt
    auto del = [&](int x) {
        cnt[c[x]]--;
        tmp -= cnt[c[x]];
    };

    // 遍歷每個查詢,透過移動指標 l 和 r 來處理區間
    for (int i = 0; i < q; i++) {
        if (que[i][1] == que[i][0]) { // 如果查詢區間長度為 0
            ans[que[i][2]] = 0;
            continue;
        }
        while (r < que[i][1]) r++, add(r); // 擴充套件右邊界
        while (l > que[i][0]) l--, add(l); // 擴充套件左邊界
        while (r > que[i][1]) del(r), r--; // 收縮右邊界
        while (l < que[i][0]) del(l), l++; // 收縮左邊界
        ans[que[i][2]] = tmp; // 記錄當前查詢的結果
    }

    // 輸出每個查詢的結果
    for (int i = 0; i < q; i++) {
        if (ans2[i] == 0 && ans[i] == 0) { // 如果分子和分母都為 0
            cout << "0/1" << endl;
            continue;
        }
        int d = __gcd(ans[i], ans2[i]); // 計算最大公約數
        cout << ans[i] / d << "/" << ans2[i] / d << endl; // 輸出結果
    }

    return 0;
}

最佳化:

過程

我們看一下下面這組資料

// 設塊的大小為 2 (假設)
1 1
2 100
3 1
4 100

手動模擬一下可以發現,r 指標的移動次數大概為 300 次,我們處理完第一個塊之後,\(l = 2, r = 100\),此時只需要移動兩次 l 指標就可以得到第四個詢問的答案,但是我們卻將 r 指標移動到 1 來獲取第三個詢問的答案,再移動到 100 獲取第四個詢問的答案,這樣多了九十幾次的指標移動。我們怎麼最佳化這個地方呢?這裡我們就要用到奇偶化排序。

什麼是奇偶化排序?奇偶化排序即對於屬於奇數塊的詢問,r 按從小到大排序,對於屬於偶數塊的排序,\(r\)從大到小排序,這樣我們的\(r\) 指標在處理完這個奇數塊的問題後,將在返回的途中處理偶數塊的問題,再向 \(n\) 移動處理下一個奇數塊的問題,最佳化了 \(r\) 指標的移動次數,一般情況下,這種最佳化能讓程式快\(30\)% 左右。

  // 按照莫隊演算法的分塊策略對查詢進行排序
    sort(que, que + q, [&](array<int, 3> a, array<int, 3> b) {
        int d = a[0] / B;
        if (a[0] / B != b[0] / B) return a[0] / B < b[0] / B;
        if (d % 2 == 1) return a[1] < b[1];
        else return a[1] > b[1];
    });

根號分治

根號分治,是暴力美學的集大成體現。與其說是一種演算法,我們不如稱它為一個常用的trick。

題目引入

首先,我們引入一道入門題目 CF1207F Remainder Problem

給你一個長度為 \(5×10^5\) 的序列,初值為 \(0\),你要完成 \(q\) 次操作,操作有如下兩種:

  1. 1 x y: 將下標為 \(x\) 的位置的值加上 \(y\)
  2. 2 x y: 詢問所有下標模 \(x\) 的結果為 \(y\) 的位置的值之和。

暴力解法

暴力1

首先有一種暴力就是按照題目所說的去做,開一個 \(5×10^5\) 大小的陣列 \(a\) 去存,\(1\) 操作就對 \(a[x]\) 加上 \(y\)\(2\) 操作就列舉所有下標模 \(x\) 的結果為 \(y\) 的位置,統計他們的和。

對於這種暴力,\(1\) 操作的時間複雜度為 \(O(1)\)\(2\) 操作的時間複雜度為 \(O(n)\),所以在最壞情況下總時間複雜度可達 \(O(nq)\)

暴力2

經過思考,我們可以發現另外一種暴力:新開一個大小為 \(n×n\) 的二維陣列 \(b\)\(b[i][j]\) 當前所有下標模 \(i\) 的結果為 \(j\) 的數的和是什麼。對於每個 \(1\) 操作,動態的去維護這個 \(b\) 陣列,在每次詢問的時候直接輸出答案即可。

對於這種暴力,\(1\) 操作的時間複雜度是列舉模數的 \(O(n)\)\(2\) 操作的時間複雜度為 \(O(1)\),總的時間複雜度為 \(O(nq)\)

根號分治策略

現在我們發現,這兩種暴力對應了兩種極端:一個是 \(1\) 操作的時間複雜度為 \(O(1)\),2 操作的時間複雜度為 \(O(n)\);另一個是 \(1\) 操作的時間複雜度是列舉模數的 \(O(n)\),2 操作的時間複雜度為 \(O(1)\)。那麼,有沒有辦法讓這兩種暴力融合一下,均攤時間複雜度,達到一個平衡呢?

其實是有的。我們設定一個閾值 \(b\)

對於所有 \(≤b\) 的數,我們動態的維護暴力 \(2\)\(b\) 陣列。每次 \(1\) 操作只需要列舉 \(b\) 個模數即可,故單次操作 \(1\) 的時間複雜度降為 \(O(b)\)

對於所有 \(>b\) 的數,我們就不在操作 \(1\) 中維護 \(b\),直接再詢問答案時暴力列舉下標即可。顯然,這 \(n\) 個下標中最多有 \(⌈n/b⌉\) 個下標對 \(x\) 取模餘 \(y\) 找到第一個 \(y\) 後每次跳 \(x\),即可做到單次操作 \(2\) 時間複雜度為 \(O(n\sqrt{b})\)

所以,總時間複雜度就成為了 \(O(q×(b+n\sqrt{b}))\)。由基本不等式可得,\(b+n/b≥2√(b×n/b)=2√n\),當 \(b=\sqrt{n}\) 時取等。所以我們只需要讓 \(b=\sqrt{n}\),就可以做到時間和空間複雜度均為\(O(q\sqrt{n})\) 的優秀演算法了,可以透過此題。

程式碼:

#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
const int N=5e5+30,mod=998244353;
typedef long long ll;
typedef pair<int,int> PII;
int n,m;
int q;
const int B=750;
int ans[B+10][B+10];
int a[N];
void solve()
{
    cin>>q;
    while(q--)
    {
        int id,x,y;
        cin>>id>>x>>y;
        if(id==1)
        {
            for(int i=1;i<=B;i++) ans[i][x%i]+=y;
            a[x]+=y;
        }
        else
        {
            if(x<=B) cout<<ans[x][y]<<endl;
            else
            {
                int temp=0;
                for(int i=y;i<=500000;i+=x)
                {
                    temp+=a[i];
                }
                cout<<temp<<endl;
            }
        }
    }

   
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _;
    _=1;
    //cin>>T;
    while(_--)
     {
         solve();
     }
     return 0;
} 

相關文章