有一种很常见的题型 就是一个在一个好长的序列中进行 乍一看都是很麻烦的那种 修改和查询。
通常这种题 都是拿高端的数据结构 轻轻松松优化查询修改复杂度写过去,可是不会怎么办??!
于是就可以利用分块这种小技巧了。
这个技能就算是带奇技淫巧的暴力吧,暴力出奇迹。
简单来说 : 这种思路就是 把序列分成很多个块, 建立块与块之间的联系,然后每次修改就只需要改一个块里的东西。
如果分成 √N 块,每块有 √N 个 , 那么每次修改 查询 的操作 就都被减到了以前的根号级别, 所以就可以水过好多题
看下面两道例题,写完就会分块了。
[HNOI2010]BOUNCE 弹飞绵羊
原链接https://www.luogu.org/problem/show?pid=1393
题的意思是,地上沿一条直线摆了N个弹簧,第i个弹簧弹力值是Ki ,代表这个弹簧会把绵羊弹飞到位置 i+Ki,若不存在 i+Ki则绵羊被弹飞。M次操作,询问操作是给定一个起点x,求小绵羊从x出发被弹几次后被弹飞,修改操作是把第i个弹簧的弹力值修改为k
N<=200000,M<=100000
这个题的标程是LCT(link-cut-tree) (有朝一日我学了会回来发题解的)
做法是先分块,(求一下每个块的l,r 每个点的belong)
block=( int)sqrt(N); cnt=(N%block)?N/block+1:N/block; for(int i=1;i<=cnt;i++) l[i]=r[i-1]+1,r[i]=l[i]+block-1; r[cnt]=N; for(int i=1,j=1;i<=cnt;j++) { belong[j]=i; if(j==r[i])i++; }
然后 算一下 每个弹簧 可以弹到 下一个块的什么位置(还有步数)
for(int i=N;i>=1;i--) { to[i]=i+a[i]; if(to[i]>r[belong[i]])step[i]=1; else step[i]=step[to[i]]+1,to[i]=to[to[i]]; }
就维护上面这些,所以每次修改只用改一个块里的东西
然后询问的时候就是一块一块的跳 累加上步数。
while(M--) { int k,x,y; read(k); if(k==1) //query { ans=0;read(x);x++; while(x<=N)ans+=step[x],x=to[x]; printf("%d\n",ans); } else //change { read(x);read(y);x++; a[x]=y; for(int i=r[belong[x]];i>=l[belong[x]];i--) { to[i]=i+a[i]; if(to[i]>r[belong[i]])step[i]=1; else step[i]=step[to[i]]+1,to[i]=to[to[i]]; } } }
1 #include<bits/stdc++.h> 2 #define MAXN 200001 3 void read(int& x) 4 { 5 x=0;char c;c=getchar(); 6 while(c<‘0‘||c>‘9‘)c=getchar(); 7 while(c>=‘0‘&&c<=‘9‘){x=x*10+c-‘0‘;c=getchar();} 8 } 9 int N,M,block,cnt,ans,a[MAXN],l[MAXN],r[MAXN],step[MAXN],to[MAXN],belong[MAXN]; 10 int main() 11 { 12 read(N); 13 for(int i=1;i<=N;i++)read(a[i]); 14 //initiation 15 block=( int)sqrt(N); 16 cnt=(N%block)?N/block+1:N/block; 17 for(int i=1;i<=cnt;i++) 18 l[i]=r[i-1]+1,r[i]=l[i]+block-1; 19 r[cnt]=N; 20 for(int i=1,j=1;i<=cnt;j++) 21 { 22 belong[j]=i; 23 if(j==r[i])i++; 24 } 25 for(int i=N;i>=1;i--) 26 { 27 to[i]=i+a[i]; 28 if(to[i]>r[belong[i]])step[i]=1; 29 else step[i]=step[to[i]]+1,to[i]=to[to[i]]; 30 } 31 //operation 32 read(M); 33 while(M--) 34 { 35 int k,x,y; 36 read(k); 37 if(k==1) //query 38 { 39 ans=0;read(x);x++; 40 while(x<=N)ans+=step[x],x=to[x]; 41 printf("%d\n",ans); 42 } 43 else //change 44 { 45 read(x);read(y);x++; 46 a[x]=y; 47 for(int i=r[belong[x]];i>=l[belong[x]];i--) 48 { 49 to[i]=i+a[i]; 50 if(to[i]>r[belong[i]])step[i]=1; 51 else step[i]=step[to[i]]+1,to[i]=to[to[i]]; 52 } 53 } 54 } 55 return 0; 56 }
完整代码
[CQOI2011]动态逆序对
原链接 https://www.luogu.org/problem/show?pid=3157
题意是有一个长度为N的序列,M次操作,每次删一个数并求逆序对个数
N<=100000,M<=50000
正经做法里有 线段树+BST的 CDQ分治的 还有主席树?
分块做法 : 每个块都存在一个树状数组里。(不懂的话去写一个树状数组求逆序对的模板=、=)
先算一下修改之前的逆序对个数,
减去其它块的贡献(贡献直接拿树状数组查询) 自己块里的贡献就直接直接遍历一遍。
复杂度差不多是N*√N*logN(应该可以卡过去)
就贴个核心操作部分的代码吧,很好理解。
// operation while(M--) { int x;cin>>x; // calc for(int i=1;i<belong[x];i++) ans-=siz-query(c[belong[x]],a[x]); for(int i=l[belong[x]];i<x;i++) if(a[i]>a[x])ans--; for(int i=x+1;i<=r[belong[x]];i++) if(a[x]>a[i])ans--; for(int i=belong[x]+1;i<=block;i++) ans-=query(c[belong[x]],a[x]-1); cout<<ans<<" "; // update add(c[belong[x]],a[x],-1); }
千言万语汇成一句话: