數列分塊總結——題目總版(hzwer分塊九題及其他題目)(分塊)


閑話

莫隊算法似乎還是需要一點分塊思想的。。。。。。於是我就先來搞分塊啦!
膜拜hzwer學長神犇%%%Orz
這九道題,每一道都堪稱經典,強力打Call!點這里進入

算法簡述

每一次考試被炸得體無完膚之后,又聽到講題目的Dalao們爆出一句

數據不大,用分塊暴力搞一下就AC了

的時候,我就會五體投地,不得翻身。。。。。。
分塊?暴力?真的有如此玄學?
直到現在我還是覺得它很笨拙,但要熟練運用(尤其是打大暴力部分分的時候),絕非易事。
沒錯,分塊是優化的暴力,更輕松地資瓷在線,是一種算法思想,主要用來維護區間信息,因此也可以當成數據結構。
當絞盡腦汁也無法發現能用自帶log的數據結構(線段樹,樹狀數組,平衡樹······)維護的時候,一定不要拋棄它。
一般的分塊題目形式,都是給你若干個區間處理操作。這些區間有長有短,直接暴力操作復雜度是\(O(n^2)\)的。
分塊就是為了解決區間過長所導致的低效而出現的。
具體做法,是把數列分成若干個小塊,每個小塊內部,每個元素的值仍然需要維護,然后再維護整個小塊的信息。
對於一個很長的操作區間,其內部一定會包含若干連續的小塊對吧。
對這些小塊,直接對小塊信息操作,不用一個個操作每個元素啦,從而省去了大量時間。
當然,區間兩頭會有不包含一個完整小塊的部分,暴力修改。然而這兩頭長度加起來不會超過塊的長度的兩倍,復雜度有保證。

常見題目類型&套路&難度分析(持續更新中)

  1. 維護塊信息總和★★☆☆☆
    最裸的一類啦。。。。。。
    例題有下面hzwer九題中的1、4

  2. 維護塊標記並進行懶處理★★★☆☆
    需要分析復雜度的思路,巧妙地利用題目特點,維護好懶標記並正確釋放,但實現難度不高
    例題有下面hzwer九題中的5、7和8

  3. 塊內維護其它結構★★★☆☆
    思路較清晰,但實現難度加大,常見的類型(我只見過的)
    —— 有序表,以便資磁塊內二分答案等操作,如下面hzwer九題中的2、3
    —— 鏈表,可以方便地插入和刪除,更可以高效完成塊與塊的合並或分裂(重構),如下面hzwer九題中的6
    —— 平衡樹,更多高級操作,題目仍在發現中(當然,可以把hzwer九題中的3、6兩題的操作合在一起)(手動滑稽)
    —— 。。。。。。

  4. 維護塊區間的信息★★★★☆
    解釋一下,這里的“塊區間”指的是第\(l\)個塊到第\(r\)個塊的信息。如果需要這樣維護的話,通常思維難度和實現難度都上了一個檔次。
    題目仍在發現並落實中。。。。。。(hzwer九題中的9當然是個火題辣,不過利用了離線我用常數更優的莫隊水了一下)

題目列表

hzwer數列分塊入門九題

hzwer的題解都很詳細了,我再寫點自己的東西吧。

分塊入門 1 by hzwer

LOJ題目傳送門
題面等於洛谷模板樹狀數組2。。。。。。
不過既然學分塊就好好學嘛。
這里的小塊信息就是一個加法標記,表示對整個區間的所有數加上標記值。
於是做法就很簡單了。
對於這個分塊長度(以下設為\(m\))的問題,我不是很清楚。對於每個具體的題目,到底該取多少呢?
方便的話,默認\(m=\sqrt n\)吧。
分析一下單次修改的復雜度。在本題中,整塊修改復雜度取決於塊的數量,復雜度\(O({n\over m})\)
不完整塊暴力修改復雜度取決於塊的長度,復雜度\(O(m)\)
根據基本不等式,\({n\over m}+m≥2\sqrt n\)當且僅當\({n\over m}=m\)時等號成立。
於是\(m=\sqrt n\)
當然,如果數據是隨機的,那么平均情況下的系數考慮一下會得到更好的\(m\)
首先,詢問區間的平均長度是\(n\over 3\)(試出來的,我太弱了不會證,歡迎Dalao指教)
然后,暴力修改的部分平均長度肯定是\(m\)\({m\over 2}×2\)),得到帶系數的復雜度\(O({n\over {3m}}+m)\)
再次用基本不等式得到更優的\(m\)\(\sqrt{{n\over 3}}\)
目測hzwer的數據是rand的,所以改了以后確實快不少。
貼下代碼吧。貌似我的代碼永遠是最短最丑的。。。。。。

#include<cstdio>
#include<cmath>
int a[50009],b[233];//b為小塊加法標記
int main()
{
	register int n,L,i,op,l,r,c;
	scanf("%d",&n);
	L=sqrt(n/3);
	for(i=0;i<n;++i)scanf("%d",&a[i]);
	for(i=0;i<n;++i)
	{
		scanf("%d%d%d%d",&op,&l,&r,&c);
		--l;--r;//為了方便,數組從0下標開始存了
		if(op)printf("%d\n",a[r]+b[r/L]);
		else
		{
			for(;l<=r&&l%L;++l)a[l]+=c;//暴力(左)
			for(;l+L-1<=r;l+=L)b[l/L]+=c;//改塊
			for(;l<=r;++l)a[l]+=c;//暴力(右)
		}
	}
	return 0;
}

分塊入門 2 by hzwer

LOJ題目傳送門
這時候,分塊成為了最優解法。
排序預處理,詢問操作塊內二分,塊外暴力。
修改操作塊內放加法標記,塊外暴力加排序重構。
平均情況下復雜度大致為\(O(n\log m+n({n\over m}\log m+m\log m))\)
利用平均系數\(m\)同樣可取\(\sqrt{{n\over 3}}\)
實際上我嘗試了一下,\(\sqrt{{n\over 4}}\)\(\sqrt n\over 2\)更快。
因為預處理中帶了個\(m\),加上修改操作中也帶有\(m\)而在上式中為了簡便被省略了。
所以\(m\)適當的更小一點。
還有一點,其實修改操作時對兩個不完整部分的重新排序,可以不用寫sort。
原塊已經排好序,修改相當於給這個有序序列部分元素加上一個相等的值。
那么現在這個序列可以分成兩個集合——被加的和沒被加的。可以看出這兩個集合按在原序列的位置中,仍然是各自有序的。
對兩個有序的序列再排序,最高效的不就是二路歸並嗎?
這樣做的話,要在有序序列中額外維護每個元素在原序列中的位置。
掃一遍這個不完整塊的時候,根據位置判斷是否被加,然后放入對應集合。最后對兩個集合歸並排序放回有序序列即可。
僅僅對單次修改操作而言,這樣做使得復雜度降至\(O({n\over m}+m)\),去掉了\(\log\)
然而這種寫法也有一點常數,也比較麻煩,我就偷了點懶QvQ
上代碼

#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
#define R register
char s[1314520];
int a[54250],b[54250],t[2333];//請忽略丑陋的數組長度定義
#define resort(P) {memcpy(b+P,a+P,L<<2);sort(b+P,b+P+L);}//重構排序
#define in(z) {while(*++p<'-');z=*p&15;while(*++p>'-')z*=10,z+=*p&15;}
int main(){
	fread(s,1,sizeof(s),stdin);
	R char*p=s-1;
	R int n,L,i,op,l,r,c,ans;
	in(n);
	L=sqrt(n)/2;
	for(i=0;i<n;++i)in(a[i]);
	for(;i%L;++i)a[i]=1e9;//人工在最末塊尾部插入inf,避免vector帶來的常數
	for(i=0;i<n;i+=L)resort(i);//初始化排序
	for(i=0;i<n;++i){
		in(op);in(l);in(r);in(c);
		--l;--r;
		if(op){
			ans=0;c*=c;
			for(;l<=r&&l%L;++l)ans+=a[l]+t[l/L]<c;//暴力(左)
			for(;l+L-1<=r;l+=L)ans+=lower_bound(b+l,b+l+L,c-t[l/L])-b-l;//塊內二分統計答案
			for(;l<=r;++l)ans+=a[l]+t[l/L]<c;//暴力(右)
			printf("%d\n",ans);
		}
		else{
			if(l/L==r/L){//在同一個塊中,暴力改完走人
				for(;l<=r;++l)a[l]+=c;
				resort(r/L*L);
				continue;
			}
			if(l%L){//暴力(左)
				for(;l%L;++l)a[l]+=c;
				resort(l-L);
			}
			for(;l+L-1<=r;l+=L)t[l/L]+=c;//直接改
			if(r>=l){//暴力(右)
				for(;r>=l;--r)a[r]+=c;
				resort(l);
			}
		}
	}
	return 0;
}

分塊入門 3 by hzwer

LOJ題目傳送門
基本思路與2差不多,除了二分統計答案的方式。
因此就沒什么好說的啦,代碼跟上面一樣丑,就不貼了

分塊入門 4 by hzwer

LOJ題目傳送門
題面等於洛谷模板線段樹1。。。。。。
相比1,再加上塊內總和的維護就好啦,代碼也不貼了

分塊入門 5 by hzwer

LOJ題目傳送門
其實分塊不是正解,我還是慣性思維想到了線段樹。
反正開方不會超過四次,那可以用帶log的數據結構維護。
考慮線段樹,弄個標記表示當前節點區間已全部變為0或1,假如兩個子區間都打標記了,那么當前區間也打上,以后區間開方碰到有標記就結束掉。
這樣復雜度是對的,\(O(N\log N)\)帶上一個不小的常數,但總比分塊好點。。。。。。
分塊的話也就是在塊內搞同樣意義的標記嘛!似乎還要寫鏈表,不是很輕松的暴力。。。。。。我還是太懶了
線段樹代碼(放在這里極其不和諧

#include<cstdio>
#include<cmath>
#define R register
#define G c=getchar()
inline void in(R int&z){
	R char G;
	while(c<'-')G;
	z=c&15;G;
	while(c>'-')z*=10,z+=c&15,G;
}
const int M=3000000;
int le[M],mi[M],ri[M],s[M],t[M];
#define lc u<<1
#define rc u<<1|1
#define pushup s[u]=s[lc]+s[rc],t[u]=t[lc]&t[rc]//上傳和以及標記
void build(R int u,R int l,R int r){//建樹
	le[u]=l;ri[u]=r;mi[u]=(l+r)>>1;
	if(l==r){
		in(s[u]);
		if(s[u]<2)t[u]=1;
		return;
	}
	build(lc,l,mi[u]);
	build(rc,mi[u]+1,r);
	pushup;
}
void update(R int u,R int l,R int r){//更新,略超出模板范圍
	if(t[u])return;
	if(le[u]==ri[u]){
		s[u]=sqrt(s[u]);
		if(s[u]<2)t[u]=1;
		return;
	}
	if(r<=mi[u])update(lc,l,r);
	else if(l>mi[u])update(rc,l,r);
	else update(lc,l,mi[u]),update(rc,mi[u]+1,r);
	pushup;
}
int ask(R int u,R int l,R int r){//查詢完全是模板
	if(l==le[u]&&r==ri[u])return s[u];
	if(r<=mi[u])return ask(lc,l,r);
	else if(l>mi[u])return ask(rc,l,r);
	else return ask(lc,l,mi[u])+ask(rc,mi[u]+1,r);
}
int main(){
	R int n,op,l,r,c;
	in(n);
	build(1,1,n);
	while(n--){
		in(op);in(l);in(r);in(c);
		if(op)printf("%d\n",ask(1,l,r));
		else update(1,l,r);
	}
	return 0;
}

分塊入門 6 by hzwer

LOJ題目傳送門
總覺得像平衡樹基本操作。。。。。。
這里引入了一個新的概念——重構。
為了方便,我手寫了一下鏈表套鏈表(滑稽)
就是塊與塊之間用鏈表相連,塊中的每個元素也用鏈表連起來。
我采用了區間過大重構的方法。
這樣的話當區間過大時,直接新開一個塊插入在塊的鏈表中,再讓其指向原塊的中點位置元素,省了挺多事的。
查詢的話就維護每個塊的大小,從前往后掃,以此確定某點在哪一個塊中,再在塊內挨個找。
還有一個小小的idea,可以讓靠前的塊更大,靠后的塊略小,復雜度被平均了,會降低一些(只是貌似帶來了更大的常數。。。。。。)
代碼

#include<cstdio>
#include<cmath>
#define G c=getchar()
#define in(z) G;\
	while(c<'-')G;\
	z=c&15;G;\
	while(c>'-')z*=10,z+=c&15,G
const int B=1009,N=200009;
int s[B],neb[B],he[B],ne[N],v[N];
//neb塊鏈表,he塊首元素,ne元素鏈表,注意區分
int main()
{
	register int n,L,i,j,b,p,pb,op,l,r;
	register char c;
	in(n);L=sqrt(n);
	for(b=i=j=0;i<n;++i,++j){
		in(v[i]);
		if(j==L){
			j=0;
			neb[b]=b+1;
			he[++b]=i;
		}
		ne[i]=i+1;
		++s[b];
	}
	p=n-1;pb=b;//p新點,pb新塊
	while(n--){
		in(op);in(l);in(r);in(j);
		if(op){//查詢,先掃塊,再掃元素
			for(b=0;s[b]<r;r-=s[b],b=neb[b]);
			for(i=he[b],--r;r;i=ne[i],--r);
			printf("%d\n",v[i]);
		}
		else{
			for(b=0;s[b]<l;l-=s[b],b=neb[b]);
			if(--l){
				for(i=he[b],--l;l;i=ne[i],--l);
				ne[++p]=ne[i],ne[i]=p;
			}//找到插入位置並插入,注意特判塊頭
			else ne[++p]=he[b],he[b]=p;
			v[p]=r;
			if(++s[b]>=L<<1){//重構,把塊砍兩半
				s[++pb]=s[b]>>=1;
				neb[pb]=neb[b];neb[b]=pb;//神奇的鏈表指來指去
				for(i=he[b],l=L-1;l;i=ne[i],--l);
				he[pb]=ne[i];
			}
		}
	}
	return 0;
}

分塊入門 7 by hzwer

LOJ題目傳送門
題面等於洛谷模板線段樹2(不是模板,是大火題!)
已經敲完線段樹2和Tree II,對此題失去了興趣(其實是因為我太弱了,真的怕放標記又寫掛。。。。。。)

分塊入門 8 by hzwer

LOJ題目傳送門
思路像5,但代碼不易,尤其是每次暴力左右不完整部分、破壞了塊的同一個值的時候,還要釋放標記,寫起來是真心累。。。。。。
長長的代碼

#include<cstdio>
#include<cmath>
#include<iostream>
using namespace std;
#define G ch=getchar()
#define in(z) G;\
	while(ch<'-')G;\
	z=ch&15;G;\
	while(ch>'-')z*=10,z+=ch&15,G
int a[100009],t[1009];
int main(){
	register int n,L,i,j,l,r,c,bel,ber,ans;
	register char ch;
	in(n);L=sqrt(n);
	for(i=0;i<n;++i){in(a[i]);}
	for(j=0;j<n;++j){
		in(l);in(r);in(c);--l;//所有下標減了1,並強行轉化成左閉右開
		bel=l/L;ber=r/L;ans=0;
		//下面真的是一大堆討論,為了減少代碼量,把很多情況合並到一起處理了
		if(bel!=ber){
			if(l%L){//不完整(左)
				if(t[bel]){
					if(c==t[bel]){//與標記相等
						ans+=(bel+1)*L-l;l=(bel+1)*L;
						goto M;
					}
					for(i=bel*L;i<l;++i)a[i]=t[bel];//不相等,放標記
					t[bel]=0;
					for(i=l,l=(bel+1)*L;i<l;++i)a[i]=c;//暴力改
				}
				else for(i=l,l=(bel+1)*L;i<l;++i)
					a[i]==c?++ans:a[i]=c;//暴力
			}
			else --bel;
		  M:while(++bel<ber){//完整
				l+=L;
				if(t[bel]){
					if(c==t[bel]){
						ans+=L;
						continue;
					}
				}
				else for(i=l-L;i<l;++i)
						 a[i]==c?++ans:a[i]=c;
				t[bel]=c;//printf("bel%d ans%d\n",bel,ans);
			}
		}
		if(l==r)goto N;//無不完整(右)
		if(t[bel]){//在不完整的同一塊中,或者不完整(右)
			if(c==t[bel]){
				ans+=r-l;
				goto N;
			}
			for(i=bel*L;i<l;++i)a[i]=t[bel];//兩邊都要放
			for(i=l;i<r;++i)a[i]=c;
			for(i=min((bel+1)*L,n)-1;i>=r;--i)a[i]=t[bel];
			t[bel]=0;
		}
		else for(i=l;i<r;++i)
			a[i]==c?++ans:a[i]=c;
	  N:printf("%d\n",ans);
	}
	return 0;
}

持續更新中。。。。。。


免責聲明!

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



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