分塊

分塊

這是第一篇博客。嘿嘿嘿!話不多說,直接開始。
首先來說一下什麼是分塊:
       分塊,顧名思義,即將數據切分成塊。那麼,這有什麼意義呢?其實,分塊是一種優雅的暴力
這裏給出一道例題:洛谷P2801。https://www.luogu.org/problemnew/show/P2801
       以此題爲例:
       有一組數據,共有n項,我們通常以根號n作爲劃分標準,即每一塊有根號n項數據(爲保持原有數據,需要開設新的數組)。以下是分塊的初始化:

int n,  a[maxn]//n爲數據的規模,a數組存儲原始數據
int b[maxn]int  l[maxn], r[maxn], belong[maxn],  block, num;
//b數組是進行分塊操作的數組,初始時是a數組的一個副本

void bulid(void) {
	block = int (sqrt(n));		//block爲每一塊所包含的元素個數
	num = n / block;		//num爲數組最終被分塊之後的塊數
	if (n%block) {
		num++;
	}
	for (int i = 1; i < num; i++) {		//默認下標從1開始,第num塊單獨處理,因爲第num塊數組元素無法確定
		l[i] = (i - 1) * block + 1;		//l數組存儲每一塊數據的起始點下標
		r[i] = i * block;				//r數組存儲每一塊數據的結束下標
		sort(b+l[i], b+r[i]+1);
	}
	l[num] = (num - 1)*block + 1;
	r[num] = n;
	sort(b+l[num], b+r[num]+1);		//對塊進行分類

	for (int i = 1; i <= n; i++) {
		belong[i] = (i-1) / block+1;	//belong數組存儲每一個元素所屬的下標
	}
}

       此題如果使用暴力,必然超時,那分塊又是如何來達到這優雅的暴力的呢?
       此題的關鍵在於頻繁地對某一區間的數據進行訪問。衆所周知,對有序數據的訪問在很多時候比對無序數據的訪問更爲高效,比如搜索(有序數組的二分搜索必然比無序數組的遍歷更爲高效)。因此,我們對每一塊都進行排序。(此步預處理,雖會消耗時間和空間,但在數據量大,且對區間操作頻繁的時候,會在不久的將來比暴力省下不少時間,可謂是未雨綢繆啊!)
       首先請看對數據的更新操作:
       大致分爲如下兩種情況:
       1:需要更新的區間包含多個塊
       2:需要更新的區間同屬於一個塊
在這裏插入圖片描述
       對於第一種情況,區間之間的完整塊可直接利用數組來記錄區間的變化情況(此處利用add數組來記錄整一塊數據的變化情況),而對於非完整塊,則只需對原數組進行跟新即可,同時更新之後需要對非完整塊所屬的塊進行排序,以維持其有序性。

void reset(int x) {	//對非完整區間進行數據跟新之後,需要保持其有序性
	for (int i = l[x]; i <= r[x]; i++) {
		b[i] = a[i];
	}
	sort(b + l[x], b + r[x] + 1);
}

void update(int left,int right,int val) {
	if (belong[left] == belong[right]) {	//第二種情況
		for (int i = left; i <= right; i++) {
			a[i] += val;
		}
		reset(belong[left]);
		return;
	}
	//第一種情況
	for (int i = left; i <= r[belong[left]]; i++) {	//左端非完整塊
		a[i] += val;
	}
	reset(belong[left]);	//保持有序性
	
	for (int i = belong[left] + 1; i < belong[right]; i++) {	//中間區塊
		add[i] += val;	//此處的add數組記錄了此塊區域的數據至此爲止的變化量,add數組是分塊優雅的一處體現
	}					

	for (int i = l[belong[right]]; i <= right; i++) {	右端非完整塊
		a[i] += val;
	}
	reset(belong[right]);	//保持有序性
}

       接下來是訪問數據:由於每一塊都是有序的,因此對塊內的更新可以利用二分來縮短時間。對於第一種情況,區間之間的完整塊利用b數組來完成訪問,而非完整塊,則利用原數組進行訪問,可見非完整塊最多隻會有兩塊。對於第二種情況,只需對原數組訪問即可。(以下給出此題的訪問操作)

int ask(int left, int right, int val) {
	int sum = 0;
	if (belong[left] == belong[right]) {		//第二種情況
		for (int i = left; i <= right; i++) {
			if (a[i] + add[belong[i]] >= val) {	//原數據加上變動量纔是此時的數據值,注意!
				sum++;
			}
		}
		return sum;
	}
	//第一種情況
	for (int i = left; i <= r[belong[left]]; i++) {	//左端非完整區塊
		if (a[i] + add[belong[i]] >= val) {
			sum++;
		}
	}
	//中間完整塊
	for (int i = belong[left] + 1; i < belong[right]; i++) {	//此處以塊進行遍歷
		sum +=fin(i, val-add[i]);		//在塊內配合以二分
	}									//速度遠大於普通暴力
	
	for (int i = l[belong[right]]; i <= right; i++) {	//右端非完整區塊
		if (a[i] + add[belong[i]] >= val) {
			sum++;
		}
	}
	return sum;
}

       至此,此題講解完畢。分塊的核心思想應該闡述清楚。

       總結:之所以要分塊,是爲了能在區間進行更爲高效的操作, 同時也要注意不能讓他上升到整個區間,不然就沒有意義了。 終於寫完第一篇了,好累。。。。。。雖然寫的不太美觀,也沒什麼樣式,以後再慢慢改善吧。