莫隊
莫隊算法(Mo's algorithm)莫濤隊長發明的算法,尊稱莫隊。
先膜一下莫隊\(\%\%\%\)莫濤 - 知乎
思路A:two pointers處理
two pointers處理
是一種優美的暴力。
例如此題:P3901 數列找不同
現有數列 \(A_1,A_2,\ldots,A_N\),\(M\)個詢問 \((L_i,R_i)\),詢問 \(A_{L_i} ,A_{L_i+1},\ldots,A_{R_i}\)是否互不相同。
\(N,M\le 10^5\)
(以下取\(N,M\)同階)
擴展一下這個問題,變成"求\((L{i},R_i)\)區間內有多少對數字相同".
如果完完全全暴力的話,開類似於桶排序的桶,每次在\((L,R)\)的區間內更新桶,並計算答案,復雜度\(O(N^3)\).
用莫隊①優化一下這個過程:
- 用兩個指針\(pl\)和\(pr\),兩個指針所指的標號內是我們維護的區間。於是我們可以僅移動這兩個指針來維護區間。
- 每一步移動的復雜度是\(O(1)\).
- 用\(pans\)表示這個區間內的答案
對於移動指針的過程,假設要將指針移動到\((l,r)\):
- 對於移動左指針(\(pl\to l\))
- ①若\(pl > l\),表示當前區間左端點短所求,則一步步向左移動左端點。
- 此時:每移動左端點一次,相當於向區間內增加一個\(a[pl-1]\),對答案的貢獻為當前已有的\(a[pl-1]\)的個數,即\(buc[a_{[pl-1]}]\).
- 故此處為
add(--pl)
.
- ②若\(pl<l\),表示當前區間左端點長於所求,則一步步向右移動左端點。
- 此時:每移動左端點一次,相當於向區間內減少一個\(a[pl]\),對答案的貢獻為當前已有的\(a[pl]\)的個數\(-1\),即\(buc[a_{[pl]}]\)\(-1\).
- 故此處為
del(pl++)
.
- ①若\(pl > l\),表示當前區間左端點短所求,則一步步向左移動左端點。
- 對於移動右指針(\(pr\to r\))
- 若\(pr < r\),與上①相同
- 為
add(++pr)
.
- 為
- 若\(pr > r\),與上②相同
- 為
del(pr--)
.
- 為
- 若\(pr < r\),與上①相同
- 關於
add
:在計算后要將\(buc++\),故為qans += buc[a[k]]++
. - 關於
del
:先\(-1\)再計算,故為qans -= --buc[a[k]];
將每一次進行add()
或del()
稱為一次擴展。
另外,\(pl\)和\(pr\)初值應賦為\(1\)和\(0\),此時表示擴展內沒有任何元素。
關於擴展的代碼:
ll pans;int pl=1,pr;
inline void add(int k){pans += buc[a[k]]++;}
inline void del(int k){pans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
return pans;
}
此時一次擴展的復雜度是\(O(1)\)的。
這樣可以將復雜度壓縮到\(O(M*N)\).
但是它還會TLE。如何進一步優化?
思路A+
對於以上算法,復雜度的上限是什么?
假設有以下\(N=10^5\)的\((L_i,R_i)\)查詢數據:
1 1
100000 100000
1 1
100000 100000
………
會發生什么?
每次操作,都會將\(pl:1\to N\space ,\space pr:1\to N\)或\(pl:N\to 1\space,\space pr:N\to1\)。
這樣會出現很多很多次無用的擴展。
考慮如何優化這些擴展:
莫隊的思路是分塊。
取正整數\(B\),把序列每\(B\)個分為一段,即分段長度是\(B\).
對於左端點在同一塊內的相鄰詢問,右端點遞增,一共至多擴展\(N\) 次,左端點每次詢問至多擴展\(B\) 步;對於左端點不在同一塊內的相鄰詢問,至多有\(\frac nB\) 個。復雜度為\(O(N\frac NB + BN)\) 次擴展的復雜度。
利用均值不等式,\(N\frac NB+BN \ge2\sqrt{N\frac NB*BN}=2N\sqrt N\),當且僅當 $B =\sqrt N $時等號成立。
故取\(B=\sqrt N\),最優復雜度為\(O(N\sqrt N)\) 次擴展的復雜度。
這樣分塊后,對於詢問的區間,按左端點所在塊的編號為第一關鍵字,右端點為第二關鍵字從小到大排序。按順序求每個區間的答案,每次直接從上一個區間暴力擴展移動到這個區間。
所以可以知道:莫隊一定是離線的。
代碼實現:
開一個\(bel[i]\)數組記錄\(i\)所在塊的編號,易得\(bel[i] = \frac i B + 1\).
用結構體記錄每次查詢的區間信息和編號。
按以上方法排序后,每次操作將所得值還原為原順序最后輸出即可。
struct ask{int l,r,id;}q[N];
inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
bool ans[N];
main:{
len = sqrt(n);
for(int i:1->n)
bel[i] = i / len + 1;
for(int i:1->m)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i:1->m)
ans[q[i].id]= calc(q[i].l,q[i].r);
for(int i:1->m)
print(ans[i]);
}
總復雜度為\(O(N\sqrt N)\).
數列找不同AC代碼:
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
using namespace std;
const int INF = 0x3f3f3f3f,N = 1e5+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ;char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? -ret : ret;
}
int n,m;
int a[N],buc[N],bel[N];
int len;
ll qans;int pl=1,pr;
inline void add(int k){qans += buc[a[k]]++;}
inline void del(int k){qans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
return qans;
}
struct ask{int l,r,id;}q[N];
inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
bool ans[N];
signed main(){
n = read() , m = read();
len = ceil(sqrt(n));
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i / len + 1;
for(int i = 1 ; i <= m ; i ++)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i = 1 ; i <= m ; i ++){
int l = q[i].l , r = q[i].r , id = q[i].id;
ans[id]= !calc(l,r);
}
for(int i = 1 ; i <= m ; i ++)
printf("%s\n",ans[i]?"Yes":"No");
return 0;
}
基本的莫隊已經結束了。我們來看一道例題:P1494 [國家集訓隊] 小Z的襪子
給定序列\(A_n\),每次詢問查詢\((L_i,R_i)\)區間內任選兩個數,選到相同數字的概率。
\(n\le5\times 10^5\)
把上面一題的擴展"求\((L{i},R_i)\)區間內有多少對數字相同"稍作修改即可。
答案為\(\frac{calc(l,r)}{C^2_{r-l+1}}\)。
約分時,上下同除\(\gcd\)就可以。
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
using namespace std;
const int INF = 0x3f3f3f3f,N = 5e4+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ;char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? -ret : ret;
}
int n,m;
int a[N],buc[N],bel[N],len;
ll qans;int pl=1,pr;
inline void add(int k){qans += buc[a[k]]++;}
inline void del(int k){qans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
// printf("(%d,%d):%lld\n",pl,pr,ans);
return qans;
}
struct ask{int l,r,id;}q[N];
inline bool operator < (ask a,ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
ll gcd(ll x,ll y){return y ? gcd(y,x%y):x;}
ll ans[N][2];
signed main(){
n = read() , m = read();
len = ceil(sqrt(n));
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i/len + 1;
for(int i = 1 ; i <= m ; i ++)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i = 1 ; i <= m ; i ++){
int l = q[i].l , r = q[i].r , id = q[i].id;
if(l == r){ans[id][0] = 0,ans[id][1] = 1;continue;}
ans[id][0] = calc(l,r),ans[id][1] = 1ll * (r-l+1) * (r-l) / 2;
ll k = gcd(ans[id][0],ans[id][1]);
ans[id][0] /= k,ans[id][1] /= k;
}
for(int i = 1 ; i <= m ; i ++)
printf("%lld/%lld\n",ans[i][0],ans[i][1]);
return 0;
}
優化
對於普通的莫隊,還是存在一些可優化的地方的。下面我們來具體分析。
奇偶優化
如圖(紅箭頭表示\(pl\)的移動,綠箭頭表示\(pr\)的移動)
可以發現,對於左端點進入新的一個區間時,右端點需要從\(N\)處回到最左邊,再跑回到最右邊。這樣也是進行了很多次多余的擴展。
提供一種奇偶優化的方案:
對於分塊標號為奇數的,按\(p[i].r\)升序排列,反之偶數按\(p[i].r\)降序排列。效果如下:
只需將排序一處的代碼修改:
- 如果\(a.l\)與\(b.l\)在同一個塊內,則:
- 如果所在塊編號是奇數,則按\(a.l<b.l\)排列;
- 如果所在塊編號是偶數,則按\(a.l>b.l\)排列。
- 如果不在同一個塊內,則左端點編號小的在前。
所以代碼為:
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? (bel[a.l] & 1 ? a.r < b.r : a.r > b.r): a.l < b.l;}
這個優化是優化在常數,不過可以使大數據快一倍。
帶修莫隊
前面我們說到,莫隊一定是離線的。但是如果遇到一些題目,要求修改?
例如【P1903 [國家集訓隊]數顏色 / 維護隊列】
有\(A_N\)的序列和\(M\)次操作,每次操作進行查詢\((L,R)\)區間內不同的數字個數或將\(A_p\)修改為\(x\).
\(N,M \le 1.5*10^5\).
題里要求必須支持修改。
我們來考慮如何修改。
對於普通的莫隊,我們的操作是:
- 每次維護\(pl\)與\(pr\)和\((L,R)\)的位置。
那么對於修改呢?
莫隊提供的方案是:增加一個時間軸\(T\).
- 可以增加一個變量\(now\),表示當前處理的詢問之前執行過多少次修改。
- 對於每一次查詢,記錄當前查詢之前進行多少修改。
這樣,可以把每次擴展改成:
inline int calc(int l,int r,int id,int t){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
while(now < t)mod(++now,id);
while(now > t)mod(now--,id);
return pans;
}//其中的6,7行為帶修莫隊新增。
其中,我們在擴展需要額外傳入\(id,t\)兩個參量。
對於\(mod\)(modify)函數,寫成如下:
inline void mod(int k,int id){
if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
pans ...... ;
swap(mo[k].v,a[mo[k].p]);
}
首先,所對應的修改位置在當前查詢的區間內才需要修改\(pans\). 修改\(pans\)的操作因題而定。
比如:例題,則為pans += !buc[mo[k].v]++ , pans -= !--buc[a[mo[k].p]];
,表示分別對修改前和修改后進行答案貢獻計算。
最后的swap
比較巧妙:直接接將序列內的值和修改操作的值進行調換,這樣能保證多次修改,進行(修改->還原->修改\(\cdots\))的操作。
此時,我們仍要修改排序。
- 考慮普通莫隊的排序:先比較\(a.l,b.l\),再比較\(a.r,b.r\).
- 那么:對於添加了一維的帶修莫隊,就可以寫成:
- 先比較\(a.l,b.l\),再比較\(a.r,b.r\),最后比較\(a.t,b.t\).
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
復雜度分析
等以后慢慢寫……
所以代碼:
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
using namespace std;
const int INF = 0x3f3f3f3f , N = 1.4e5+5 , M = 1e6+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ; char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9')) ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? - ret : ret;
}
int n,m;
int a[N];
int pl=1,pr,pans,len,bel[N];
int buc[M],now;
int ans[N];
struct ask{int l,r,id,t;}q[N];int qcnt;
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
struct mdf{int p,v;}mo[N];int mcnt;
inline void mod(int k,int id){
if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
pans += !buc[mo[k].v]++,
pans -= !--buc[a[mo[k].p]];
swap(mo[k].v,a[mo[k].p]);
}
inline void add(int k){pans += !buc[a[k]]++;}
inline void del(int k){pans -= !--buc[a[k]];}
inline int calc(int l,int r,int id,int t){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
while(now < t)mod(++now,id);
while(now > t)mod(now--,id);
return pans;
}
signed main(){
n = read() , m = read();
len = pow(n,2.0/3);
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i / len + 1;
for(int i = 1 ; i <= m ; i ++){
char ch[2];int x,y;
scanf(" %s %d %d",ch,&x,&y);
if(ch[0] == 'Q')q[++qcnt] = (ask){x,y,qcnt,mcnt};
else mo[++mcnt] = (mdf){x,y};
}
sort(q+1,q+qcnt+1);
for(int i = 1 ; i <= qcnt ; i ++)
ans[q[i].id] = calc(q[i].l,q[i].r,i,q[i].t);
for(int i = 1 ; i <= qcnt ; i ++)
printf("%d\n",ans[i]);
return 0;
}
剩下的優化還不會,等以后再來更新吧……
例題
P2709 小B的詢問【板子中的板子】
P1972 [SDOI2009] HH的項鏈【玄學優化】