莫隊演算法(Mo's Algorithm)
前置知識
最好是會一點線段樹,不會也沒有關係
正文
我們都知道,維護區間資訊的時候通常會用到各種線段樹,因為其本身具有的性質可以在很快的速度內完成各種操作。但是這是基於我們上推(pushup)和下推(pushdown)這兩種操作可以在O(1)的時間內完成的基礎。如果這兩個操作本身需要的時間不是常數級別的化我們可能需要考慮其他的方法。莫隊演算法可以有效地解決一部分這樣的問題。更加精確地來講,普通莫隊演算法可以解決的問題有以下兩種特徵:
- 只有查詢,沒有修改
- 從區間\([ l , r]\)擴充到區間\([ l\pm1,r\pm1]\)的時間消耗是常數級別的
當然莫隊演算法還有很多其他的變種,類似於樹上莫隊,代修莫隊等等。這裡我們只介紹最簡單的基礎莫隊。
簡單來講,莫隊演算法的思想就是用特殊的順序對詢問進行排序使得程式執行時間加快。
打個比方,我們現在要維護區間和,並且假設大家都沒學過字首和,樹狀陣列和線段樹,那麼我們能想道的最樸素的演算法就是對於每一個詢問都老老實實地從 l 遍歷到 r 。但是我們發現了一個問題,對於兩組詢問, \([l_1,r_1]\)和\([l_2,r_2]\)來講,如果出現了下圖的情況:
我們是沒必要在第二個詢問的時候重新遍歷的,因為區間 \([l_1,r_2]\)的答案實際上已經包含在了\([l_1,r_]\),也就是第一次詢問裡面了,並且從\(l_1\)擴充套件到\(l_2\),\(r_1\)擴充套件到\(r_2\)的時間消耗可能比較低(因為從位置\(i\)擴充套件答案到\(i\pm1\)的位置的時間消耗是O(1)的)。這就是莫隊演算法的思想,還是比較簡單的。
那麼可能有人要問了,如果我們的查詢區間是類似於: \([1,2]\),\([n-1,n]\),\([1,3]\),\([n-2,n]\)這樣的,相鄰詢問跨度很大並且沒有交集的情況該怎麼半呢?這裡我們可以對詢問進行排序。可以把每一個和詢問抽象成平面上面的兩個點,那麼詢問\([l_1,r_1]\)到\([l_2,r_2]\)的時間消耗就是\(ABS(l_1-l_2)+ABS(r_1-r_1)\),也就是這兩個點之間的曼哈頓距離。那麼我們對詢問的排序也就很明顯了,就是求這幅圖的最小曼哈頓距離生成樹。
但是這麼寫的化碼量實在是有一些大(計算幾何題的程式碼複雜程度大家肯定也是有目共睹的),那麼我們能不能找到一個不錯的替代品呢?
分塊,yyds
我們簡單地對區間進行分塊,然後對於左區間在同一塊內的群問按照右區間從小到大排序,否則就按照左區間進行排序,這樣的化就可以有效地加快程式執行速度。
例題: 小B的詢問
簡單來說就是要你維護每一個數字在區間\([l,r]\)內部出現次數的平方和。
用線段樹是會T的,但是我們新加一個數到答案裡面的時間是常數的,所以我們可以考慮用莫隊
首先我們需要把詢問給儲存起來,最好還是另外存一個k來表示第k個詢問,因為最後輸出的時候還是會檢查你的順序的
struct Q{
int l,r,k;
}q[maxn];
然後就正常讀入啊什麼的,順便分個塊
scanf("%d %d %d",&n,&m,&k);
int siz=n/sqrt(m*2/3);//似乎說是這麼分會快一點
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[i]=i/siz;
}
for(int i=1;i<=m;i++){
scanf("%d %d",&q[i].l,&q[i].r);
q[i].k=i;
}
接著寫一下排序的cmp函式:
bool cmp(Q a,Q b){
if(pos[a.l]==pos[b.l]) return a.r<b.r;
return pos[a.l]<pos[b.l];
}
在後面就是莫隊的基本操作了:
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
while(q[i].l<l) add(--l);
while(q[i].r<r) sub(r--);
while(q[i].l>l) sub(l++);
while(q[i].r>r) add(++r);
ans[q[i].k]=rsl;
}
這裡的add函式和sub函式就是計算新增加/減少的一個數對答案的貢獻的函式,後面一定要記得按照每一個詢問的k值來村答案
void add(int n){
cnt[a[n]]+=1;
rsl+=(cnt[a[n]])*(cnt[a[n]])-(cnt[a[n]]-1)*(cnt[a[n]]-1);
}
void sub(int n){
cnt[a[n]]-=1;
rsl-=(cnt[a[n]]+1)*(cnt[a[n]]+1)-(cnt[a[n]])*(cnt[a[n]]);
}
最後再輸出一下就好了
AC程式碼全放送:
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5;
struct Q{
int l,r,k;
}q[maxn];
int a[maxn],cnt[maxn],pos[maxn],n,m,k,ans[maxn],rsl;
bool cmp(Q a,Q b){
if(pos[a.l]==pos[b.l]) return a.r<b.r;
return pos[a.l]<pos[b.l];
}
void add(int n){
cnt[a[n]]+=1;
rsl+=(cnt[a[n]])*(cnt[a[n]])-(cnt[a[n]]-1)*(cnt[a[n]]-1);
}
void sub(int n){
cnt[a[n]]-=1;
rsl-=(cnt[a[n]]+1)*(cnt[a[n]]+1)-(cnt[a[n]])*(cnt[a[n]]);
}
int main(void){
scanf("%d %d %d",&n,&m,&k);
int siz=n/sqrt(m*2/3);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[i]=i/siz;
}
for(int i=1;i<=m;i++){
scanf("%d %d",&q[i].l,&q[i].r);
q[i].k=i;
}
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
while(q[i].l<l) add(--l);
while(q[i].r<r) sub(r--);
while(q[i].l>l) sub(l++);
while(q[i].r>r) add(++r);
ans[q[i].k]=rsl;
}
for(int i=1;i<=m;i++){
printf("%d\n",ans[i]);
}
}
額外的(常數)優化
這裡我們還可以用一個小技巧,就是如果兩個\(l\)所在的塊是奇數塊的化\(r\)就要從小到大排序,如果是偶數塊的化就從大到小。這樣的化對於在同一個塊內的詢問我們就可以在回來的之後順便計算下一次詢問的值了。