看了hzwer的博客,受益匪淺,於是來分享一下自己的想法。
首先,分塊是用來干啥的呢?簡單點說,就是一個處理數據結構的高級暴力。 如果我們想要修改一個序列並查詢,一個一個的模擬顯然是太慢了的,但有些東西又不能用線段樹來維護,那么怎么辦呢?這時候就分塊就派上用場了。
先沿用幾個黃學長使用的概念:
- 整塊:在一個區間操作時,完整包含於區間的塊
-
不完整的塊:在一個區間操作時,只有部分包含於區間的塊,即區間左右端點所在的兩個塊
下面講一下分塊方法:
- 將一個序列分成m塊,每個塊有(n/m)個元素。
- 在修改區間時對於詢問左端點所在的區間和右端點所在的區間暴力修改元素。
- 對於左端點和右端點之間的區間直接打上區間修改標記。
- 查詢也是對於不在一個整塊的區間暴力修改,在整塊中的區間通過區間標記查詢。
因為被分成了m塊,所以時間復雜度約為O(n/m+m),根據均值不等式,m取sqrt(n)時時間復雜度取得最小值(當然根據題目不同也可以改變分塊的大小)。
然后將塊分開了之后,就要記錄每個塊的信息了。那么按照每塊sqrt(n)個元素,會分成這樣:
- 每個塊內有sqrt(n)個元素(用block代替)
- 每個塊i的所涵蓋的區間的左端點下標為(i-1)*block+1,右端點下標為i*block(左右都是閉區間)。
- 序列最后面可能有一些零散的元素,屬於第block+1塊,沒有被分入整塊中。
通過這些信息,我們就可以確定一個區間所屬於哪些塊,以及一個塊所涵蓋的區間。
分塊的代碼比較靈活,可以記錄各種各樣的東西,下面以黃學長的分塊入門1為例:
題目大意就是給出一個長為 n 的數列,以及 n 個操作,操作涉及區間加法,單點查值。
對於區間加法,我們可以將修改的區間中完整的區間打上區間標記,對於在不完整的塊上的元素直接暴力修改。
查詢則直接輸出該元素的值加上它所在的區間的標記值。
1 #include<bits/stdc++.h>
2 using namespace std; 3 const int N=50000+5; 4
5 int n, m; 6 int w[N];//w[i]記錄下標為i的元素的值
7 int b[N];//b[i]記錄下標為i的元素屬於第b[i]個塊
8 int block;//每個塊有block個元素
9 int lazy[N];//記錄對整塊的操作
10
11 int gi(){//讀入優化
12 int ans = 0 , f = 1; char i = getchar(); 13 while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();} 14 while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();} 15 return ans * f; 16 } 17
18 void add(int x,int y,int z){ 19 for(int i=x;i<=min(b[x]*block,y);i++) w[i] += z;//修改x所在的不完整塊
20 for(int i=b[x]+1;i<=b[y]-1;i++) lazy[i] += z;//對整塊進行修改
21 if(b[x] != b[y])//因為如果x和y在同一個塊內,則在修改x所在的不完整塊的時候就已經修改過了
22 for(int i=(b[y]-1)*block+1;i<=y;i++) w[i] += z;//修改y所在的不完整塊
23 } 24
25 int main(){ 26 int flag, x, y, val; n = gi(); 27 block = sqrt(n); 28 for(int i=1;i<=n;i++) w[i] = gi(); 29 for(int i=1;i<=n;i++) b[i] = (i-1)/block+1;//處理每個元素所在的塊
30 for(int i=1;i<=n;i++){ 31 flag = gi(); x = gi(); y = gi(); val = gi(); 32 if(flag == 0) add(x,y,val); 33 else printf("%d\n",w[y]+lazy[b[y]]); 34 } 35 return 0; 36 }
下面分析一下其他的分塊入門題:
分塊入門2:
給出一個長為 n 的數列,以及 n 個操作,操作涉及區間加法,詢問區間內小於某個值 x 的元素個數。
既然要統計小於某個值的元素個數,那么同樣可以直接暴力統計出在不完整的塊上的元素小於 x 的個數。而對於整塊中的元素,可以直接另外開一個數組對整塊排序,並對這個塊二分查找。還有要注意的細節是不能直接對原數組進行排序,而要另外開一個數組來排序,在二分查找的時候要用這個排序后的數組進行查找。因為如果直接對原數組進行排序的話,就無法保存原位置的值,會導致查詢不完整區塊時出現問題。
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=50000+5; 4 5 int w[N], b[N], v[N]; 6 int block, n, lazy[N]; 7 8 void updata(int blo){ 9 int l = (blo-1)*block+1 , r = min(blo*block,n); 10 for(int i=l;i<=r;i++) v[i] = w[i]; 11 sort(v+l , v+r+1); 12 } 13 14 void add(int x,int y,int val){ 15 for(int i=x;i<=min(y,b[x]*block);i++) w[i] += val; 16 for(int i=b[x]+1;i<=b[y]-1;i++) lazy[i] += val; 17 for(int i=(b[y]-1)*block+1;i<=y && b[x]!=b[y];i++) w[i] += val; 18 updata(b[x]); updata(b[y]); 19 } 20 21 int query(int x,int y,int z){ 22 int res = 0 , whole = 0 , small = 0; 23 for(int i=x;i<=min(y,b[x]*block);i++) 24 if(w[i]+lazy[b[x]] < z) res++ , small++; 25 for(int i=b[x]+1;i<=b[y]-1;i++){ 26 int l = (i-1)*block+1 , r = i*block , pos = l-1 , st = l-1; 27 while(l <= r){ 28 int mid = (l+r >> 1); 29 if(v[mid]+lazy[i] < z) l = mid+1 , pos = mid; 30 else r = mid-1; 31 } 32 res += pos-st; whole += pos-st; 33 } 34 for(int i=(b[y]-1)*block+1;i<=y && b[x]!=b[y];i++) 35 if(w[i]+lazy[b[y]] < z) res++ , small++; 36 //printf("ans=%d small=%d whole=%d\n",res,small,whole); 37 printf("%d\n",res); 38 } 39 40 int main(){ 41 //freopen("a1.in","r",stdin); 42 //freopen("ans.out","w",stdout); 43 ios::sync_with_stdio(false); 44 int x, y, z, flag; 45 cin >> n; block = sqrt(n); 46 for(int i=1;i<=n;i++) cin >> w[i]; 47 for(int i=1;i<=n;i++) b[i] = (i-1)/block+1; 48 memcpy(v,w,sizeof(v)); 49 for(int i=1;i<=block;i++) 50 sort(v+(i-1)*block+1,v+i*block+1); 51 for(int i=1;i<=n;i++){ 52 cin >> flag >> x >> y >> z; 53 if(flag == 0) add(x,y,z); 54 else query(x,y,z*z); 55 } 56 return 0; 57 }
分塊入門3:
給出一個長為 n 的數列,以及 n 個操作,操作涉及區間加法,詢問區間內小於某個值 x 的前驅(比其小的最大元素)。
和上一題比較像,同樣可以將塊內排序,通過二分的方式加速查找前驅,並暴力查詢在不完整塊上的元素是否能存在前驅。
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=100000+5; 4 const int inf=2147483647; 5 6 int n, block; 7 int w[N], v[N], b[N]; 8 int lazy[N]; 9 10 int gi(){ 11 int ans = 0 , f = 1; char i = getchar(); 12 while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();} 13 while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();} 14 return ans * f; 15 } 16 17 void updata(int blo){ 18 int l = (blo-1)*block+1 , r = blo*block; 19 for(int i=l;i<=r;i++) v[i] = w[i]; 20 sort(v+l , v+r+1); 21 } 22 23 void add(int x,int y,int z){ 24 for(int i=x;i<=min(y,b[x]*block);i++) w[i] += z; 25 for(int i=b[x]+1;i<=b[y]-1;i++) lazy[i] += z; 26 for(int i=(b[y]-1)*block+1;i<=y && b[x]!=b[y];i++) w[i] += z; 27 updata(b[x]); updata(b[y]); 28 } 29 30 int query(int x,int y,int z){ 31 int res = -1; 32 for(int i=x;i<=min(b[x]*block,y);i++) 33 if(w[i]+lazy[b[x]] < z) res = max(res , w[i]+lazy[b[x]]); 34 for(int i=b[x]+1;i<=b[y]-1;i++){ 35 int l = (i-1)*block+1 , r = i*block , pos = 0 , mid; 36 while(l <= r){ 37 mid = (l+r >> 1); 38 if(v[mid]+lazy[i] < z) l = mid+1 , pos = mid; 39 else r = mid-1; 40 } 41 if(pos && v[pos]+lazy[i] < z) res = max(res , v[pos]+lazy[i]); 42 } 43 for(int i=(b[y]-1)*block+1;i<=y && b[x]!= b[y];i++) 44 if(w[i]+lazy[b[y]] < z) res = max(res , w[i]+lazy[b[y]]); 45 return res; 46 } 47 48 int main(){ 49 int x, y, z, flag; n = gi(); block = sqrt(n); 50 for(int i=1;i<=n;i++) w[i] = gi(); 51 for(int i=1;i<=n;i++) b[i] = (i-1)/block+1; 52 memcpy(v,w,sizeof(v)); 53 for(int i=1;i<=block;i++) 54 sort(v+(i-1)*block+1 , v+i*block+1); 55 for(int i=1;i<=n;i++){ 56 flag = gi(); x = gi(); y = gi(); z = gi(); 57 if(flag == 0) add(x,y,z); 58 else printf("%d\n",query(x,y,z)); 59 } 60 return 0; 61 }
分塊入門4:
給出一個長為 n 的數列,以及 n 個操作,操作涉及區間加法,區間求和。
既然涉及到了區間求和,那么我們可以在整塊上多記錄一個變量sum[],來儲存整個塊的和(不包括對於整個塊的修改操作)。在讀入之后記錄每個塊的總和,在修改不完整區塊的時候同時修改該塊的和。然后對於整個塊的修改用一個變量存下來。最后在查詢區間的時候也是暴力兩邊零散的元素,將中間整塊的直接計入答案。
分塊入門5:
給出一個長為 n 的數列,以及 n 個操作,操作涉及區間開方,區間求和。
這題要求的修改操作是開方,用其他的數據結構很顯然是很麻煩的(霧)。那么我們仔細想一下,顯然不論是什么數字,在多次開方后的值都會變成0或1。所以我們可以在修改塊的時候直接暴力修改,在修改整塊的時候就判斷這個塊內是否只有0和1。如果是,那么這個塊之后的操作都可以省略掉了,然后像上一題一樣記錄一個塊的和,然后按套路輸出。
分塊入門6:
給出一個長為 n 的數列,以及 n 個操作,操作涉及單點插入,單點詢問,數據隨機生成。
研究中。。。。
