分块
分块,顾名思义,就是将序列分成一小块一小块的来处理和维护。
我们把一段当成一个整体,只记录整体有关的信息,这就是分块。
首先,我们考虑一道题。
1.区间修改,查询区间中一段区间有多少数 < k;
一种朴素算法就是,暴力从区间 L 到 R 扫过去,每遇到小于k的位置加1
没错,是不是这种方法很傻。但分块就是从这里优化过来的。
假设,我们有一个长度为 n 的序列,然后我们把他切成 sqrt(n)块,把每一块中的东西当成一个整体。(至于为什么块的大小是sqrt(n),自己可以百度一下QAQ)
几个术语: 完整块 被操作区间完全覆盖的块
不完整块 不完全覆盖的块
然后我们考虑怎么得出答案
- 首先我们要预处理每个点在哪个块,以及每个块的左右端点。
- 然后, 对于不完整的块,我们可以考虑直接暴力修改,因为元素比较少。
- 那么, 对于完整的块,我们就可以考虑打上lazy标记。
总而言之,分块就是将小块暴力,大块处理的一种方法。
然后这就可以用分块——这种看似高大上的方法做完。。。。。。比之前傻傻的暴力是不是好了很多。
下面就是预处理代码
1 for(long long i = 1; i <= n; i++){
2 pos[i] = (i-1)/len+1;//每个点所在的块
3 }
4 for(int i = 1; i <= len; i++){
5 L[i] = (i-1)*len+1;//块的左右端点
6 R[i] = i*len;
7 }
8 if(R[len] < n) L[len+1] = R[len] +1; R[len+1] = n;//最后一个块单独处理
思想讲完了,下面就该看几道题了。
P4145 花神游历各国
题意大概是给出一个长为n的序列,以及n个操作,操作有区间开方和区间求和。
我们会发现开方操作比较难,主要是维护整块时,必须一个个的去修改,那这样岂不是和不同的暴力复杂度不就差不多了吗.
然后我们接着往深里想,不难发现,一个数经过几次开方后,就会变成0或1.
如果每次区间开方只要修改不完整的块,直接暴力修改就可以了。
如果修改一些整块,我们可以只要在每个整块暴力开方后,记录一下元素是否变成了0或1,修改时直接跳过全为0或1的块
就okk了 ,这样每个元素至多开方不超过4次,复杂度刚好可以。
1 #include2 #include 79 } 80 return 0; 81 }3 #include 4 #include 5 #include 6 using namespace std; 7 const int N = 1e5+100; 8 long long n,m,k,l,r; 9 long long a[N],pos[N],L[N],R[N],sum[N]; 10 bool vis[N]; 11 void check(long long x){//检查每个块是否全部变为0或1 12 if(vis[x]) return; 13 vis[x] = 1; 14 sum[x] = 0; 15 for(int i = L[x]; i <= R[x]; i++){ 16 a[i] = sqrt(a[i]); 17 sum[x] += a[i]; 18 if(a[i] > 1) vis[x] = 0; 19 } 20 } 21 void chenge(long long l,long long r){ 22 int p = pos[l],q = pos[r]; 23 if(p == q){//在一个块内,直接暴力修改 24 for(int i = l; i <= r; i++){ 25 sum[p] -=a[i]; 26 a[i] = sqrt(a[i]); 27 sum[p] += a[i]; 28 } 29 } 30 else { 31 for(int i = p+1; i <= q-1; i++) check(i);//修改整块 32 for(int i = l; i <= R[p]; i++){//左端点的散块 33 sum[p] -=a[i]; 34 a[i] = sqrt(a[i]); 35 sum[p] += a[i]; 36 } 37 for(int i = L[q]; i <= r; i++){//右端点的散块 38 sum[q] -= a[i]; 39 a[i] = sqrt(a[i]); 40 sum[q] += a[i]; 41 } 42 } 43 } 44 long long ask(long long l,long long r){//同上面的修改操作 45 long long ans = 0; 46 int p = pos[l],q = pos[r]; 47 if(p == q) for(int i = l; i <= r; i++) ans += a[i]; 48 else { 49 for(long long i = p+1; i <= q-1; i++) ans += sum[i]; 50 for(int i =l; i <= R[p]; i++) ans+= a[i]; 51 for(long long i = L[q]; i <= r; i++) ans += a[i]; 52 } 53 return ans; 54 } 55 int main(){ 56 cin>>n; 57 memset(vis, false, sizeof(vis)); 58 int len = sqrt(n); 59 for(long long i = 1; i <= n; i++) cin>>a[i]; 60 for(long long i = 1; i <= n; i++){//预处理每个点所在的块 61 pos[i] = (i-1)/len+1; 62 } 63 for(int i = 1; i <= len; i++){每个块的左右端点 64 L[i] = (i-1)*len+1; 65 R[i] = i*len; 66 } 67 if(R[len] < n) L[len+1] = R[len] +1; R[len+1] = n; 68 for(int i = 1; i <= len+1; i++){//每个块的元素和 69 for(int j = L[i]; j <= R[i]; j++){ 70 sum[i]+=a[j]; 71 } 72 } 73 cin>>m; 74 for(long long i = 1; i <= m; i++){ 75 cin>>k>>l>>r; 76 if(l > r) swap(l,r); 77 if( k == 0) chenge(l,r); 78 else cout< endl;
然后这道题可能分块没问题会得到90到100分,(到现在我也不知道自己分块哪里出问题了。。。。)
就像这样
当然你也可以试试去写线段树,然后就会轻松AC了。emmmm最好也写写分块。
于是我不厚道的把线段树代码掏了出来
1 #include
2 #include
3 #include
4 #include
5 using namespace std;
6 const int N = 100010;
7 long long n,m,opt,l,r;
8 long long a[N];
9 struct tree{
10 struct node{
11 int lc,rc;
12 long long tag,sum;//tag表示这一段区间的值是否全小于1
13 }tr[N<<2];
14 #define l(o) tr[o].lc
15 #define r(o) tr[o].rc
16 #define tag(o) tr[o].tag
17 #define sum(o) tr[o].sum
18 void up(int o){
19 sum(o) = sum(o*2) + sum(o*2+1);
20 tag(o) = tag(o*2) & tag(o*2+1);
21 }
22 void build(int o,int L,int R){
23 l(o) = L , r(o) = R;
24 if(L == R){
25 sum(o) = a[L];
26 if(a[L] <= 1) tag(o) = 1;
27 else tag(o) = 0;
28 return ;
29 }
30 int mid = (L + R) / 2;
31 build(o*2,L,mid);
32 build(o*2+1,mid+1,R);
33 up(o);
34 }
35 void chenge(int o,int L,int R){
36 if(tag(o) == 1) return;
37 if(l(o) == r(o)){
38 sum(o) = (int) sqrt(sum(o));
39 if(sum(o) <= 1) tag(o) = 1;
40 return ;
41 }
42 int mid = (l(o) + r(o)) / 2;
43 if(L <= mid) chenge(o*2,L,R);
44 if(R > mid) chenge(o*2+1,L,R);
45 up(o);
46 }
47 long long ask(int o,int L,int R){
48 if(L <= l(o) && R >= r(o)){
49 return sum(o);
50 }
51 long long ans = 0;
52 int mid = (l(o) + r(o)) /2;
53 if(l <= mid) ans += ask(o*2,L,R);
54 if(R > mid) ans += ask(o*2+1,L,R);
55 return ans;
56 }
57 }tree;
58 int main(){
59 scanf("%ld",&n);
60 for(int i = 1; i <= n; i++) scanf("%ld",&a[i]);
61 tree.build(1,1,n);
62 scanf("%ld",&m);
63 for(int i = 1; i <= m; i++){
64 scanf("%ld%ld%ld",&opt,&l,&r);
65 if(l > r) swap(l,r);
66 if(opt == 0){
67 tree.chenge(1,l,r);
68 }
69 else{
70 printf("%ld\n",tree.ask(1,l,r));
71 }
72 }
73 return 0;
74 }
第二题 P2801 教主的魔法
这个题大致就是区间加和区间询问小于k的数。
对于每个询问,可以先将这个块排好序,然后二分查找k的值就行了。散块直接暴力查询没有排序的原序列。
怎么修改呢???
对于整块,我们可以打上 lazy 标记,查询时直接查 >= k - lazy 的值;
对于散块,直接暴力修改,在直接排个序就okk了。
第N题:P4168 蒲公英 (这道题我也不会,贼恶心)
这道题题意就是给你一段区间,然后询问区间的最小众数。
首先 a < 1e9先离散化;
然后预处理出两个数组 p[i][j] 表示第i个块到第j个块的最小众数。
s[i][j] 前i个块中 j出现的次数
如何处理呢
- 对于s, 直接每一个块扫一遍 。
- 对于 p ,双重枚举i,j开个数组暴力统计每个数出现多少次就行了。
- 对于询问操作,残块暴力搞,整块骚操作。所以对于posr - posl <= 2 的情况直接可以暴力搞出来
4.对于其他的情况,答案要么是整块中的众数,或者残块中的数。对于残块中的数,我们已经预处理了之前数字出现的前缀和,
所以能很快的求出这个数出现的次数。
5.对于整块中的众数,我们也可以很快的求出ta在残块中的次数。这样答案就可以出来了。
void Treaker()//分块预处理 { for(int i=1;i<=n;++i) for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++; for(int i=1;i<=pos[n];++i) { memset(tong,0,sizeof(tong)); for(int j=i;j<=pos[n];++j) { p[i][j]=p[i][j-1]; for(int k=L[j];k<=R[j];++k) { tong[a[k]]++; if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k]a[k]; } } } memset(tong,
0,sizeof(tong)); }
比较重要的是
p[i][j]=p[i][j-1]
因为我们需要先继承先前的区间众数,然后才能更新,否则的话就会听取 WA 声一片(据说我们学长神犇就卡在这里好几回)
if(pos[r]-pos[l]<=2) { int res=0; for(int i=l;i<=r;++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]a[i]; } for(int i=l;i<=r;++i)tong[a[i]]=0; return res; }
int res=0; for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]]; for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]]; for(int i=l;i<=R[pos[l]];++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]a[i]; } for(int i=L[pos[r]];i<=r;++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]a[i]; } int k=p[pos[l]+1][pos[r]-1]; int lin=s[pos[r]-1][k]-s[pos[l]][k]; for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k); for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k); if(lin>tong[res]||(lin==tong[res]&&k k; for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0; for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0; return res;
最后有几点要注意
- 注意离散化,否则直接RE到原地起飞。
- 注意 p 数组的转移,否则 WA 到怀疑人生。
- 注意分块的边界,否则TLE 到直接原地爆炸。
由于本蒟蒻水平太差,此篇题解参考了神犇(wjlss)的博客,如果有人看不懂的话,这里有链接QAQ wljss
最后本人代码一直炸,所以附上神犇的代码
#include#include 0,sizeof(tong)); } int ask(int l,int r) { if(pos[r]-pos[l]<=2) { int res=0; for(int i=l;i<=r;++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]#include #include #include using namespace std; int n,m,tot,len,l,r,ans; const int N=40010,M=50010,K=510; int a[N],b[N],pos[N],s[K][N],L[K],R[K],p[K][K],tong[N]; int read() { char ch;int x=0,f=1; while(!isdigit(ch=getchar())) {(ch=='-')&&(f=-f);} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } int cnt=0; void yych()//离散化 { len=sqrt(n); memset(L,0x3f,sizeof(L)); for(int i=1;i<=n;++i) { pos[i]=(i-1)/len+1; L[pos[i]]=min(L[pos[i]],i); R[pos[i]]=max(R[pos[i]],i); a[i]=read(); b[i]=a[i]; } sort(b+1,b+1+n); tot=unique(b+1,b+1+n)-b-1; for(int i=1;i<=n;++i)a[i]=lower_bound(b+1,b+1+tot,a[i])-b; } void Treaker()//分块预处理 { for(int i=1;i<=n;++i) for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++; for(int i=1;i<=pos[n];++i) { memset(tong,0,sizeof(tong)); for(int j=i;j<=pos[n];++j) { p[i][j]=p[i][j-1]; for(int k=L[j];k<=R[j];++k) { tong[a[k]]++; if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k] a[k]; } } } memset(tong,
a[i]; } for(int i=l;i<=r;++i)tong[a[i]]=0; return res; } int res=0; for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]]; for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]]; for(int i=l;i<=R[pos[l]];++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i] a[i]; } for(int i=L[pos[r]];i<=r;++i) { ++tong[a[i]]; if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i] a[i]; } int k=p[pos[l]+1][pos[r]-1]; int lin=s[pos[r]-1][k]-s[pos[l]][k]; for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k); for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k); if(lin>tong[res]||(lin==tong[res]&&k k; for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0; for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0; return res; } int main() { cin>>n>>m; yych(); Treaker(); for(int i=1;i<=m;++i) { l=read();r=read(); l=(l+ans-1)%n+1;r=(r+ans-1)%n+1; if(l>r)swap(l,r); ans=b[ask(l,r)]; printf("%d\n",ans); } return 0; }
然后你就可以AC 一道紫题(据说前几年这个还是一道黑题)
最后,分块的东西也就这么多,主要理解小块暴力,整块瞎搞就可以了。
最后附上习题。
- 作诗
- 蒲公英
- 线段树2
- 区间众数强制在线(黑题,难度比较高,有能力的可以做做)
- 定价 (河北15年省选题,思考出怎么分块就可以轻松AC了)