优雅的暴力——分块
分块,顾名思义,就是把数据分成块来处理。个人觉得和分治差别不算很大,只不过分治是把问题变得小之又小,而分块则是规定了块的大小就不再改变。
因为分块在把数据分成块之后,块之间就各司其职,互不侵犯,所以我们关心的就是两个问题,块中要进行什么操作,以及块与块之间又有什么样的联系。
先来道分块模板题 洛谷P2801
其实这题普适性可能没那么的强,但是做为模板题来说还是很有代表性的。
首先分块先要进行一些预处理,确定块的大小、确定块的数量、标记每个块的左右边界、标记每个元素所属的块、对每个块的数据进行初始化,我们可能做着做着题目会把预处理的操作会省掉,但是这里提醒一句,不要偷懒,该预处理还是要预处理,因为如果不预处理的话,每次处理都要计算一遍的话时间复杂度会变高,这时候很有可能会超时。
然后我们来处理一下上面两个问题,块中要进行什么操作,以及块与块之间又有什么样的联系。
对于这题,第一个操作是区间加,那么我们就可以判断区间包含了块的什么区域,如果是有零散的部分即不能形成一个块的话,就直接暴力,如果是有包含一整个一整个块的话,就用到线段树的思想(lazy操作)
第二个操作是求区间大于某个值的个数,可以看到我们如果是暴力做的话,就是一个一个判断,而分块之后,我们可以整个块整个块进行局部排序,然后直接二分找出,同样的,如果是有零散的部分,暴力。
这两个操作只涉及了块中操作,并不用找出块与块之间联系,所以普适性不是那么的强。
#include <bits/stdc++.h> #define ri register int #define maxn 10000007 using namespace std; int n,q,a[maxn],x,y,k,belong[maxn],L[maxn],R[maxn],d[maxn],lazy[maxn]; char ch; void build () { int block=sqrt(n),tot=n/block; if (n%block) tot++; for (ri i=1;i<=n;i++) belong[i]=(i-1)/block+1,d[i]=a[i]; for (ri i=1;i<=tot;i++) L[i]=(i-1)*block+1,R[i]=i*block; R[tot]=n; for (ri i=1;i<=tot;i++) sort(d+L[i],d+R[i]+1); } void change() { if (belong[x]==belong[y]) { for (ri i=x;i<=y;i++) a[i]+=k; for (ri i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); } else { for (ri i=x;i<=R[belong[x]];i++) a[i]+=k; for (ri i=L[belong[y]];i<=y;i++) a[i]+=k; for (ri i=L[belong[x]];i<=R[belong[x]];i++) d[i]=a[i]; for (ri i=L[belong[y]];i<=R[belong[y]];i++) d[i]=a[i]; sort(d+L[belong[x]],d+R[belong[x]]+1); sort(d+L[belong[y]],d+R[belong[y]]+1); for (ri i=belong[x]+1;i<=belong[y]-1;i++) lazy[i]+=k; } } void query() { int ans=0; if (belong[x]==belong[y]) { for (ri i=x;i<=y;i++) if (lazy[belong[x]]+a[i]>=k) ans++; } else { for (ri i=x;i<=R[belong[x]];i++) if (lazy[belong[x]]+a[i]>=k) ans++; for (ri i=L[belong[y]];i<=y;i++) if (lazy[belong[y]]+a[i]>=k) ans++; for (ri i=belong[x]+1;i<=belong[y]-1;i++) { int ll=L[i],rr=R[i],result; int weizhi=lower_bound(d+L[i],d+R[i]+1,k-lazy[i])-d; ans+=R[i]-weizhi+1; /* while (ll<=rr) { int mid=(ll+rr)>>1; if (d[mid]+lazy[i]>=k) rr=mid-1,result=R[i]-mid+1; else ll=mid+1; } ans+=result;*/ } } cout<<ans<<endl; } int main() { cin>>n>>q; for (int i=1;i<=n;i++) cin>>a[i]; build(); while (q--) { cin>>ch>>x>>y>>k; if (ch=='A') query(); else change(); } return 0; }
P3203
这道题就要考虑块与块之间有什么样的联系。
可以看到如果按照常规分块的处理之后,我们好像并没有思路,那么我们就得考虑要建立块与块之间的联系。
这题目的意思是跳到没得跳为止,结合分块,我们不就想出一个像前向星的东西,处理出当前点能跳到下个分块的哪一个点以及所需的步数。点与点之间的跳跃,就变成了块与块之间的跳跃。
而且我们在修改跳跃值时,只需要改变当前块要依靠此点来跳跃的点即可,因为我们现在是块与块之间的联系,所以一个块内的点的信息不影响块与块之间的联系。
#include <bits/stdc++.h> #define maxn 1000005 using namespace std; int n,m,i,size,k[maxn],j,a[maxn],b[maxn],op,x,y,c[maxn]; int main() { scanf("%d",&n); for (i=1;i<=n;i++) scanf("%d",&a[i]); size=sqrt(n); if (size*size<n) size++; for (i=1;i<=n;i++) if (((i-1)/size+1)*size>n) k[i]=n; else k[i]=((i-1)/size+1)*size; for (i=n;i;i--) { if (a[i]+i>k[i]) b[i]=1,c[i]=a[i]+i; else b[i]=b[a[i]+i]+1,c[i]=c[a[i]+i]; } scanf("%d",&m); while (m--) { scanf("%d%d",&op,&x); x++; if (op==1) { int sum=0; i=x; while (i<=n) { sum+=b[i]; i=c[i]; } printf("%d\n",sum); } else { scanf("%d\n",&y); a[x]=y; if (x+y>k[x]) { b[x]=1; c[x]=x+y; for (i=x-1;i>k[x]-size;i--) { if (a[i]+i>k[i]) { b[i]=1; c[i]=a[i]+i; } else { b[i]=b[a[i]+i]+1; c[i]=c[a[i]+i]; } } } else { b[x]=b[x+y]+1; c[x]=c[x+y]; for (i=x-1;i>k[x]-size;i--) { if (a[i]+i>k[i]) { b[i]=1; c[i]=a[i]+i; } else { b[i]=b[a[i]+i]+1; c[i]=c[a[i]+i]; } } } } } return 0; }
P3396
这里就要依靠分块思想来建块状数组了。
因为每个位置与给出的数取余时,就会发现十分的有规律,往往是隔两个、三个位置取同一批余数相同的数,这种就需要块状数组来处理了。
对于每一个数,我们可以预处理出每一个数在不同的数取余后,要放在哪一个块状数组,那么题目给出我们已经预处理过的数的时候,就可以直接输出,但是如果有些大的数我们可能不会先进行预处理,因为大的数如果也预处理了,我们就跟裸的暴力没什么区别。而修改操作就直接算这个数在哪个块状数组,然后改一下就好。
#include <bits/stdc++.h> #define maxn 150005 using namespace std; int i,n,m,x,y,size,j,sum,a[maxn],p[1005][1005]; char ch; int main() { cin>>n>>m; for (i=1;i<=n;i++) scanf("%d",&a[i]); size=floor(sqrt(n)); for (i=1;i<=n;i++) for (j=1;j<=size;j++) p[j][i%j]+=a[i]; while (m--) { cin>>ch>>x>>y; if (ch=='A') { if (x<=size) { cout<<p[x][y]<<endl; } else { sum=0; for (i=y;i<=n;i+=x) sum+=a[i]; cout<<sum<<endl; } } else { for (i=1;i<=size;i++) p[i][x%i]-=(a[x]-y); a[x]=y; } } return 0; }
CQOI2007
这题有个比较长的推导过程,https://ac.nowcoder.com/acm/problem/blogs/19908,可以算是十分简单易懂了。
#include <bits/stdc++.h> using namespace std; long long n,k,ans; int main() { cin>>n>>k; ans=n*k; for (int l=1,r=1;l<=n;l=r+1) { if (k/l!=0) r=min(k/(k/l),n); else r=n; ans-=1ll*((r+l)*(k/l)*(r-l+1))/2; } cout<<ans; return 0; }
TJOI2016求动态逆序和
这种题就已经算是十分贴切分块本来的难度了。
对于交换操作,我们可以看到只影响交换的那个区间的值,其他值都不会影响到,所以,我们只需考虑区间的值是否构成逆序对,而在模板题里面有个操作是找区间第K大,那么我们这题变通一下,就是找交换的两个值,直接看区间里面第几个值是大于/小于它们的,因为这样前面/后面的值才可以与之构成逆序对。(未调试完毕,思路大题如下)
#include <bits/stdc++.h> using namespace std; void build(int x) { int l=(x-1)*size+1,r=min(x*size,n); for (int i=l;i<=r;i++) { a[i]=v[i]; sort(a+l,a+r+1); } } int query(int x,int y,int vl,int vr) { int f=1,res; if (vl>vr) swap(vl,vr),f=-1; if (block[x]==block[y]) { for (int i=x;i<=y;i++) res+=(v[i]>vl && v[i]<vr); res=res<<1|1; return res*f; } for (int i=block[x]+1;i<=block[y]-1;i++) { int l=(i-1)*size+1,r=min(i*size,n); int cnt1=lower_bound(a+l,a+r+1,vl)-a-l+1; int cnt2=upper_bound(a+1,a+r+1,vr)-a-l; if (cnt1<=cnt2) res+=cnt2-cnt1+1; } for (int i=x;i<=block[x]*size;i++) res+=(v[i]>vl && v[i]<vr); for (int i=(block[y]-1)*size+1;i<=y;i++) res+=(v[i]>vl && v[i]<vr); res=res<<1|1;return res*f; } int main() { cin>>n>>m; size=sqrt(n); for (i=1;i<=n;i++) { block[i]=(i-1)/size+1; v[i]=a[i]=i; } while (m--) { cin>>x>>y; if (x>y) swap(x,y); if (x==y) { printf("%lld\n",ans); continue; } ans+=query(x,y,v[x],v[y]); printf("%lld\n",ans); swap(v[x],v[y]); build(block[x]); if (block[x]!=block[y]) build(block[y]); } return 0; }
分块难题还是比较多,因为这种区间操作的题目基本上都要带个树状数组或是线段树什么的,所以码量往往比较大容易出错。