莫隊+帶修莫隊 及優化詳解


莫隊

莫隊算法(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++).
  • 對於移動右指針(\(pr\to r\)
    • \(pr < r\),與上①相同
      • add(++pr).
    • \(pr > r\),與上②相同
      • del(pr--).
  • 關於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的項鏈【玄學優化】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM