分块学习(持续更新)
看了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 个操作,操作涉及单点插入,单点询问,数据随机生成。
研究中。。。。