分塊與莫隊

RVm1eL_o6II發表於2024-04-08

不沾樹的部落格變短了好多。

分塊

例題

這道題顯然可以使用線段樹亂搞過去,不過為了給主角面子我們假設我們不會做。

對於一些難以使用資料結構維護答案的序列問題,我們考慮暴力。但是暴力太慢了,於是人們提出了分塊。

分塊,就是把序列分成許多的小段,透過一些神秘的處理實現最佳化暴力。

並且應當保證,整塊內的答案可以 \(O(1)\) 獲取,非整塊的答案暴力獲得後得以與塊內答案處理。

考慮均值不等式,當且僅當塊數 \(tot\) 與塊長 \(num\) 都為 \(\sqrt{n}\) 時。這個暴力能被最佳化到 \(n\sqrt n\)

線段樹1

考慮用分塊解決線段樹問題。\(O(n\sqrt n)\) 肯定不如 \(O(n\ logn)\) 優秀,但是大多數情況這倆都能過。

分塊的區間操作也參考了線段樹的 \(lazytag\)。使用 \(flag_i\) 表示第 \(i\) 個塊的整體加數,邊角就暴力改。\(sum_i\) 為第 \(i\) 個塊的元素和,按照線段樹的寫法寫就好了。

有一個技巧:不建議在分塊中使用下放上傳操作,而是彙總答案對每個部分都加上 \(flag\)

#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
#define MAXM 320
using namespace std;
int n,q;
int num,tot,f[MAXN],siz[MAXM];
int a[MAXN],flag[MAXM],sum[MAXM];
inline void modify(int l,int r,int val){
	for(int i=l;i<=min(r,f[l]*num);i++)a[i]+=val,sum[f[i]]+=val;
	if(f[l]!=f[r]){
		for(int i=(f[r]-1)*num+1;i<=r;i++)a[i]+=val,sum[f[i]]+=val;
	}
	for(int i=f[l]+1;i<=f[r]-1;i++)flag[i]+=val;
}
inline int getans(int l,int r){
	int res=0;
	for(int i=l;i<=min(r,f[l]*num);i++)res+=a[i]+flag[f[i]];
	if(f[l]!=f[r]){
		for(int i=(f[r]-1)*num+1;i<=r;i++)res+=a[i]+flag[f[i]];
	}
	for(int i=f[l]+1;i<=f[r]-1;i++)res+=sum[i]+flag[i]*siz[i];
	return res;
}
signed main(){
	scanf("%lld%lld",&n,&q);
	num=sqrt(n);
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1,sum[f[i]]+=a[i],++siz[f[i]];
	for(int id=1,opt,l,r,k;id<=q;id++){
		scanf("%lld%lld%lld",&opt,&l,&r);
		if(opt==1){
			scanf("%lld",&k);
			modify(l,r,k);
		}
		else printf("%lld\n",getans(l,r));
	}
	return 0;
}

在資料隨機的情況下,分塊可能會體現更優秀的時間複雜度。

分塊的思想簡單粗暴,但是如何有效地設計並處理分塊來有效降低複雜度與其他技巧的相容性極強,並不好想,這導致分塊題目難度上限高。

彈飛綿羊

這道題用動態樹解決,但是我們不會動態樹,使用分塊解決。

考慮將序列分成 \(\sqrt n\) 塊,\(jump_{i},to_{i}\) 分別表示在第 \(i\) 個位置放置羊,它跳出 \(i\) 所在塊的步數與跳出後的位置。

這樣統計答案就是 \(O(\sqrt n)\) 了。由於是單點修改 \(loc\) 的權值,只需要重置 \(loc\) 所在塊的資訊即可。具體地,從 \(loc\) 開始往回掃塊:

\[jump_i=jump_{i+a_i}+1,to_i=to_{i+a_i} \]

這樣修改也是 \(O(\sqrt n)\)

教主的魔法

如何處理塊內有多少元素大於 \(val\)?先不去想塊的問題,假如只有一個序列,如何處理序列中多少元素大於 \(val\)?顯然有排序+二分的 \(O(nlog\ n)\) 做法。於是考慮將每個塊排序,在整塊內二分,在邊角上暴力。查詢複雜度 \(O(\sqrt n+\sqrt nlog\sqrt n)\)

考慮修改,修改操作只會對邊角所在的塊造成影響,需要暴力重造兩個塊,複雜度 \(O(\sqrt n+\sqrt nlog\sqrt n)\)

總複雜度 \(O(Q(\sqrt n+\sqrt nlog\sqrt n))\)。非常的抽象。

注意最後的塊可能是不完整的,我們不想處理塊的大小導致的巨量細節,使用 vector 能處理地稍微方便一些。

#include<bits/stdc++.h>
#define MAXN 1000005
#define MAXM 1005
#define int long long
using namespace std;
int n,q;
int num,tot,f[MAXN];
vector<int>val[MAXM];
int a[MAXN],block[MAXM];
inline void reset(int id){
	val[id].clear();
	for(int i=(id-1)*num+1;i<=min(n,id*num);i++)val[id].push_back(a[i]);
	sort(val[id].begin(),val[id].end());
}
signed main(){
	scanf("%lld%lld",&n,&q);
	num=sqrt(n);
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		val[f[i]].push_back(a[i]);
	}
	for(int i=1;i<=tot;i++)sort(val[i].begin(),val[i].end());
	for(int id=1,l,r,v;id<=q;id++){
		char opt[3];
		scanf("%s",opt+1);
		scanf("%lld%lld%lld",&l,&r,&v);
		if(opt[1]=='M'){
			for(int i=l;i<=min(r,f[l]*num);i++)
				a[i]+=v;
			reset(f[l]);	
			if(f[l]!=f[r]){
				for(int i=(f[r]-1)*num+1;i<=r;i++)
					a[i]+=v;
				reset(f[r]);
			}
			for(int i=f[l]+1;i<=f[r]-1;i++)
				block[i]+=v;
		}
		else{
			int res=0;
			for(int i=l;i<=min(r,f[l]*num);i++)if(a[i]+block[f[i]]>=v)++res;
			if(f[l]!=f[r])for(int i=(f[r]-1)*num+1;i<=r;i++)if(a[i]+block[f[i]]>=v)++res;
			for(int i=f[l]+1;i<=f[r]-1;i++){
				int xx=v-block[i];
				res+=val[i].size()-(lower_bound(val[i].begin(),val[i].end(),xx)-val[i].begin());
			}
			printf("%lld\n",res);
		}
	}
	return 0;
}

蒲公英

一般題目中出現“輸入是加密的”說明要求強制線上(不然怎麼輸入啊)。特地提出“l>r時交換”說明資料是隨機的。這時候分塊被卡的機率會很低。

這道題使用邪道做法:字首和塊元素個數,暴力掃塊內元素,複雜度 \(O(n*num)\)。不過能過。

現在提供這種題的正經做法。

作詩

一般採用 \(ans_{i,j}\) 表示從第 \(i\) 個塊到第 \(j\) 個塊的答案和,每次查詢暴力掃邊角在將答案合併。

不過分塊題的答案肯定不是可以簡單合併的,在這道題中邊角的數與塊內的數一旦有重合就會影響總個數。\(ans_{i,j}\) 只能簡單維護答案,如何判重?

考慮再使用 \(sum_{i,j}\) 表示塊 \(i\) 開始元素 \(j\) 的字首和。不過這種題一般要使用字尾和,會在之後解釋。

對於一段區間 \([l,r]\),將其分為邊角 \(a,b\) 與塊區間 \([x,y]\) 後,對於元素 \(k\) 可以這樣合併:

\[ans=ans_{i,j} \]

\[\forall k\in a,b: \]

\[ans-=\sum_{num_a+num_b+\sum_{i=x}^{y}num_{b_i}(\sum_{i=x}^{y}num_{b_i}\neq0)}k \]

即原本算在 \([x,y]\) 答案中,但實際彙總個數不為偶數的數需刪去。

\[ans+=\sum_{\sum_{i=x}^{y}num_{b_i}=0,num_a+num_b\in2\Z}k \]

即只在邊角出現的偶數個元素需要加上

\[ans+=sum_{num_a+num_b,\sum_{i=x}^{y}num_{b_i}\in2\Z+1}k \]

即邊角與塊內個數都為奇數的元素要加上,因為合起來就是偶數了。

並且處於複雜度我們不能列舉元素種類,注意到大部分塊內的答案是不變的,只有邊角 \(a,b\) 影響到的答案才需要進行上述的修改。

所以在掃邊角時將可能對答案有影響的元素統計併入棧。處理塊答案時再拿出來就行,\(|a|,|b|\le 2\sqrt n\),故計算也是 \(O(\sqrt n)\)。總複雜度 \(O(m\sqrt n)\)

不過如何預處理?

顯然預處理最多隻能是 \(O(n\sqrt n)\) 的。我們按塊下標作起點,變掃變統計當前時段有多少元素個數為偶數,到塊邊界的時候存下,注意到求和是順帶的,不過變成了字尾和。

但是我們發現總複雜度為 \(O((n+m)\sqrt n)\)。這顯然偏離了分塊的初衷即基本不等式最佳化暴力。令塊長為 \(B\) 則有 \(n/B\) 個塊,複雜度 \(O(n^2/B+mB)\)

\[n^2/B+mB\ge2\sqrt{n^2m} \]

當且僅當 \(n^2/B=mB\Longrightarrow B=\sqrt{n^2/m}\) 時有最小。

不過這還不是最優解,因為在處理問題時複雜度會有常數,所以會設計出各種時空配置的分塊。

這裡只提供初步最佳化。

#include<bits/stdc++.h>
#define MAXN 100005
#define MAXM 1005
using namespace std;
int n,c,m;
int num,tot;
int f[MAXN];
int sum[MAXM][MAXN],block[MAXM][MAXM];
int a[MAXN],ans;
int val[MAXN],stac[MAXN],top;
int main(){
	scanf("%d%d%d",&n,&c,&m);
	while(num*num<n*n/m)++num;
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=tot;i++){
		int cnt=0;
		for(int j=(i-1)*num+1;j<=n;j++){
			++sum[i][a[j]];
			if((sum[i][a[j]]%2==1)&&(sum[i][a[j]]>1))cnt--;
			else if(sum[i][a[j]]%2==0)cnt++;
			if(f[j]!=f[j+1])block[i][f[j]]=cnt;
		}
	}
	for(int id=1,l,r;id<=m;id++){
		scanf("%d%d",&l,&r);
		l=(l+ans)%n+1,r=(r+ans)%n+1;
		if(l>r)swap(l,r);
		ans=0;
		if(f[r]==f[l]){
			for(int i=l;i<=r;i++){
				++val[a[i]];
				stac[++top]=a[i];
			}
			while(top){
				if(val[stac[top]]){
					if(val[stac[top]]%2==0)++ans;
					val[stac[top]]=0;
				}
				--top;
			}
			printf("%d\n",ans);
			continue;
		}
		if(f[l]+1<=f[r]-1)ans=block[f[l]+1][f[r]-1];
		for(int i=l;i<=f[l]*num;i++){
			++val[a[i]];
			stac[++top]=a[i];
		}
		for(int i=(f[r]-1)*num+1;i<=r;i++){
			++val[a[i]];
			stac[++top]=a[i];
		}
		while(top){
			int v=stac[top];
			if(val[v]){
				if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==0)&&(val[v]%2==1))--ans;
				if(((sum[f[l]+1][v]-sum[f[r]][v])==0)&&((val[v]%2)==0))++ans;
				if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==1)&&(val[v]%2==1))++ans;
				val[v]=0;
			}
			--top;
		}
		printf("%d\n",ans);
	}
	return 0;
}

對於蒲公英那道題,做法是相似的,維護區間眾數與數字前字尾和,顯然只有邊角數字會影響答案,掃一遍邊角數字重新統計即可。

顏色

\([l,r]\) 內顏色 \([a,b]\) 數的平方之和,(最開始想分二維的塊),區間顏色權值之和必須要先算單色個數,再平方,再相加,這樣的複雜度是很高的。

考慮 \(sum_{i,j,k}\) 表示第 \(i\)\(j\) 塊內顏色 \(k\) 的出現次數,\(block_{i,j,k}\) 表示對這個數字平方並字首和。

統計元素個數時,碎塊元素數 \(val_1\) 與整塊元素數 \(val_2\) 對整塊平方數 \(v\) 的貢獻:

\[ans=v+2val_1val_2+val_1^2 \]

為了少列舉一次顏色,單個增加時有:

\[ans=v+2(val_{now}+val_2)+1=(val_{now}+v+1)^2-(val_{now}+v)^2 \]

設塊長為 \(B\) 我們發現:好像預處理就會爆啊!列舉塊+顏色彙總少說是 \(O(m(\frac{n}{B})^2)\) 的。

不過還是可以不等式的。

\[qB+mn^2/B^2\ge2\sqrt{qn^2m/B} \]

\[B= \ ^3\sqrt{\frac{n^2m}{q}} \]

為了方便處理令 \(n=m=q\),此時塊數不超過 50,即可預處理。

#include<bits/stdc++.h>
#define MAXN 20005
#define MAXM 50
#define N 50005
using namespace std;
int n,m,q;
int num,tot;
int a[N],f[N];
int val[MAXN];
int sum[MAXM][MAXM][MAXN],block[MAXM][MAXM][MAXN];
int ans,top,stac[N];
int main(){ 
	scanf("%d%d%d",&n,&m,&q);
	while((long long)num*num*num<=(long long)n*n)++num;
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=tot;i++){
		for(int j=i;j<=tot;j++){
			for(int k=(j-1)*num+1;k<=min(n,j*num);k++)++val[a[k]];
			for(int k=1;k<=m;k++)sum[i][j][k]=val[k],block[i][j][k]=val[k]*val[k]+block[i][j][k-1];
		}
		for(int j=1;j<=m;j++)val[j]=0;
	}
	for(int id=1,l,r,A,B;id<=q;id++){
		scanf("%d%d%d%d",&l,&r,&A,&B);
		if(ans)l^=ans,r^=ans,A^=ans,B^=ans;
		ans=0;
		if(l>r)swap(l,r);
		if(A>B)swap(A,B);
		if(f[r]==f[l]){
			int res=0;
			for(int i=l;i<=r;i++){
				if(a[i]>=A&&a[i]<=B)res+=val[a[i]]*2+1,++val[a[i]],stac[++top]=a[i];
			}
			while(top){
				if(val[stac[top]])val[stac[top]]=0;
				--top;
			}
			printf("%d\n",res);
			ans=res;
			continue;
		}
		ans=block[f[l]+1][f[r]-1][B]-block[f[l]+1][f[r]-1][A-1];
		for(int i=l;i<=min(r,f[l]*num);i++){
			if(a[i]>=A&&a[i]<=B)
				ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
		}
		for(int i=(f[r]-1)*num+1;i<=r;i++){
			if(a[i]>=A&&a[i]<=B)
				ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
		}
		while(top){
			if(val[stac[top]])val[stac[top]]=0;
			--top;
		}
		printf("%d\n",ans);
	}
	return 0;
}

莫隊

給定一段序列瘋狂求區間和。

幼年不會線段樹時,我想到不同區間的答案是可以線性轉移的,且如果讓區間儘可能的近,那麼轉移就儘可能的快,當時就想到將查詢序列離線排序處理答案。

這就是莫隊的主要思路,非常簡單,不過我當時不會分塊劃分,所以複雜度是假的。

事實上更快的莫隊需要更小的總跳躍次數,嚴格上講要對查詢序列跑曼哈頓距離最小生成樹。不過可以用分塊搞到 \(O(n\sqrt n)\)。即對查詢序列左端點按塊遞增排序,右端點做第二關鍵字遞增。

證明:排序後左端點按塊遞增,左端點塊相同時右端點遞增,故單個塊的右端點最多跳 \(O(n)\) 次,塊內左端點跳左端點最多跳 \(O(B)\)

總複雜度\(O(\frac{n^2}{B}+qB)\),取 \(n=q\),則 \(B=\sqrt n\) 時有最小複雜度 \(O(n\sqrt n)\)

這也揭示了莫隊的使用範疇

  • 必須可以離線。
  • 必須可以快速(\(O(1),O(logn)\))跳下標轉移答案。
  • 不得有複雜修改。

小B的詢問

這個題如果用分塊會導致記憶體爆炸。不過沒要求線上,考慮莫隊。

設當前元素 \(k\)\(val_k\) 個,則新增一個 \(k\) 時對答案的貢獻:

\[res+=2val_k+1=(val_k+1)^2-val_k^2 \]

減少一個時的貢獻:

\[res-=2val_k-1=val_k^2-(val_k-1)^2 \]

然後就沒了。

#include<bits/stdc++.h>
#define MAXN 50005
#define int long long
using namespace std;
int n,m,k;
int a[MAXN],f[MAXN];
int num,tot;
struct node{
	int l,r,id;
}q[MAXN];
inline bool cmp(node x,node y){
	if(f[x.l]==f[y.l])return x.r<y.r;
	return f[x.l]<f[y.l];
}
int ll=1,rr,res;
int ans[MAXN],val[MAXN];
inline void Add(int x){
	res+=val[a[x]]*2+1;
	++val[a[x]];
}
inline void Del(int x){
	res-=(val[a[x]]-1)*2+1;
	--val[a[x]];
}
signed main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	num=sqrt(n);
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
	for(int i=1;i<=m;i++){
		scanf("%lld%lld",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	sort(q+1,q+1+m,cmp);
	for(int i=1,l,r;i<=m;i++){
		l=q[i].l,r=q[i].r;
		while(l<ll)--ll,Add(ll);
		while(l>ll)Del(ll),++ll;
		while(r<rr)Del(rr),--rr;
		while(r>rr)++rr,Add(rr);
		ans[q[i].id]=res;
	}
	for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
	return 0;
}

小Z的襪子

在長為 \(siz\) 的區間 \([l,r]\) 中有 \(\frac{siz(siz-1)}{2}\) 種雙選取法,假設其中的 \(k\) 種元素各有 \(val_i\) 個,則機率:

\[ans=\frac{\sum_{i=1}^k\frac{val_i(val-1)}{2}}{\frac{siz(siz-1)}{2}} \]

增減一個元素 \(k\) 對分子的影響為 \(val_k\)\(val_k-1\)

然後就沒了。

作業

莫隊的擴充套件性比較強。

本題中,開兩棵 BIT 維護值域 \([l,r]\) 間的元素種數與元素個數。

時間複雜度 \(O(n\sqrt n\ logn)\)

不過在根號演算法中再使用 \(log n\) 複雜度的東西會大幅增加時間複雜度,稍微卡一卡就 T 了。

考慮最佳化,我們發現如果在維護值域的過程中引入分塊,即將值域分塊,維護塊內外的答案。那麼單次塊上的查詢是 \(\sqrt n\) 的。

不過只需在修改後查詢,也就是說莫隊操作的複雜度與查詢的複雜度是獨立的,總複雜度 \(O(n\sqrt n)\)

數顏色

不考慮修改的話這道題算莫隊板題。不過現在有修改了。

這個東西叫做帶修莫隊,即支援簡單的修改操作。

莫隊對每個詢問區間 \([l,r]\) 組成的二元組建系,沿軸進行 \(O(1)\) 的新增刪除操作。現在考慮時間維度,一次位於 \(t\) 的修改只會對 \(t_x>t\) 的查詢操作造成影響。

具體地,在這道題中,在剛才的基礎上只有位於查詢區間的修改才能對該區間的答案造成影響。

所以參考 \(l X r\) 二維平面,排序後讓莫隊在三維上移動。

#include<bits/stdc++.h>
#define MAXN 1500000
#define int long long
using namespace std;
int n,m;
int num,tot;
int f[MAXN],a[MAXN];
struct node{
	int l,r,t,id;
}qa[MAXN],qm[MAXN];
int va,vm;
inline bool cmp(node x,node y){
	return f[x.l]==f[y.l]?(f[x.r]==f[y.r]?x.t<y.t:x.r<y.r):(x.l<y.l);
}
int ll=1,rr,tt,res;
int val[MAXN];
inline void Add(int x){
	if(!val[a[x]])++res;
	++val[a[x]];
}
inline void Del(int x){
	--val[a[x]];
	if(!val[a[x]])--res;
}
inline void Upd(int x,int tim){
	if(qa[x].l<=qm[tim].l&&qm[tim].l<=qa[x].r){
		Del(qm[tim].l);
		int v=qm[tim].r;
		if(!val[v])++res;
		++val[v];
	}
	swap(a[qm[tim].l],qm[tim].r);
}
int ans[MAXN];
signed main(){
	scanf("%lld%lld",&n,&m);
	while(n*n>num*num*num)++num;
	tot=(n+num-1)/num;
	for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1,l,r;i<=m;i++){
		char opt[5];
		scanf("%s%lld%lld",opt+1,&l,&r);
		if(opt[1]=='Q'){
			++va;
			qa[va].l=l,qa[va].r=r,qa[va].t=vm;
			qa[va].id=va;
		}
		else{
			qm[++vm].l=l,qm[vm].r=r;
		}
	}
	sort(qa+1,qa+1+va,cmp);
	for(int i=1,l,r,t;i<=va;i++){
		l=qa[i].l,r=qa[i].r,t=qa[i].t;
		while(l>ll)Del(ll++);
		while(l<ll)Add(--ll);
		while(r>rr)Add(++rr);
		while(r<rr)Del(rr--);
		while(t<tt)Upd(i,tt--);
		while(t>tt)Upd(i,++tt);
		ans[qa[i].id]=res;
	}
	for(int i=1;i<=va;i++)printf("%lld\n",ans[i]);
	return 0;
}

更高階的技巧會在足夠強後續寫。

相關文章