這是第一篇博客。嘿嘿嘿!話不多說,直接開始。
首先來說一下什麼是分塊:
分塊,顧名思義,即將數據切分成塊。那麼,這有什麼意義呢?其實,分塊是一種優雅的暴力。
這裏給出一道例題:洛谷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; }
至此,此題講解完畢。分塊的核心思想應該闡述清楚。
總結:之所以要分塊,是爲了能在區間進行更爲高效的操作, 同時也要注意不能讓他上升到整個區間,不然就沒有意義了。 終於寫完第一篇了,好累。。。。。。雖然寫的不太美觀,也沒什麼樣式,以後再慢慢改善吧。