莫隊
參考文章:
莫隊細講——從零開始學莫隊
莫隊演算法——從入門到黑題
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 x y
: 將下標為 \(x\) 的位置的值加上 \(y\)。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;
}