分块是一种应用很广的根号算法c++
有一个别名为“优雅的暴力”算法
这篇文章偏向于介绍分块入门,而且讲解了几种OI中经典的分块套路数组
(由于几道例题我作的时间间隔有点远,因此可能会有几种奇奇怪怪的不一样的码风,请强迫症患者谨慎食用)优化
给一个序列,支持区间加,区间查询ui
\(N<=100000\) \(Ai<=1e9\) \(M<=100000\)spa
M为操做数code
(其实就是线段树1)排序
线段树和树状数组的板子题get
可是这里不讲线段树和树状数组的作法string
咱们回归本真,思考一下使用暴力解决该题
使用前缀和维护
\(O(n)\)修改 \(O(1)\)查询
直接加
\(O(1)\)修改 \(O(n)\)查询
固然。都会TLE(雾
考虑优化,发现两种暴力都是有明显的复杂度瓶颈(一个在查询一个在修改)
咱们是否是能够以牺牲一种操做的复杂度为代价下降另外一种操做的复杂度?(固然,总的复杂度须要比原先的复杂度低)
这就须要使用到分块的思想
块:将整个序列划分为多段序列,这些序列被称之为块
块的大小:块内元素个数(通常为\(\sqrt(n)\),可是能够根据不一样的题目使用均值不等式计算出更优的块大小,通常用于卡常。平时用\(\sqrt{n}\)就能够了),记为\(block\)
块的个数:\(num=n/block\)。即为\(\sqrt{n}\),这也是为何咱们块的大小要选择\(\sqrt{n}\)的缘由,让大小和块数尽量均衡,使查询,修改的复杂度都为\(\sqrt{n}\)
整块:在查询/修改操做中,一整个块都被包含在操做的区间中(如对于区间[1…10],块[1…3]即为整块)
散块:在查询/修改操做中,部分元素被包含在操做区间中的块(如对于区间[1…10],块[10]即为散块)
显然,对于每一个操做,散块最多2个,整块最多\(\sqrt{n}\)个
一个序列,咱们把它分红\(\sqrt{n}\)块
而后对于每一个块分别统计前缀和
查询的时候咱们须要使用\(\sqrt{n}\)的时间来统计答案
查询的时候是给出一个区间[l…r],由于咱们把整个序列分红\(\sqrt{n}\)块,因此对于[l…r]这个区间,咱们须要统计的整个的块的数目不超过\(\sqrt{n}\)个,对于两边边边角角的部分,咱们直接使用暴力,也只须要\(\sqrt{n}\)时间
总的复杂度为\(O(\sqrt{n})\)
对于修改操做,复杂度仍然存在瓶颈,咱们仍然须要\(O(n)\)修改每一块的前缀和
引入一个东西:懒标记
就是线段树下推时的那个玩意
对于一个区间内所包含的整块
咱们只须要给当前块的懒标记加一下就好,查询的时候记得把每块的懒标记的值也给加上就好
对于散块,咱们暴力修改原数组,而后统计一下前缀和就好,由于散块最多只有两个,因此复杂度也是\(O(\sqrt{n})\)
总的修改复杂度为\(O(\sqrt{n})\)
因此对于一开始的那道题,使用分块对暴力进行优化咱们能够在\(O((n+m)\sqrt{n})\)
能够说分块是一种优雅的暴力
分块思想:总体维护,局部暴力
考虑到我就这么空泛的去讲估计也很虚,因此放个代码,代码内有必定量注释(并很少,请结合上文理解)
#include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f #define il inline namespace io {//读优 #define int long long #define in(a) a=read() #define out(a) write(a) #define outn(a) out(a),putchar('\n') #define I_int int inline I_int read() { I_int x = 0 , f = 1 ; char c = getchar() ; while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; } while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; } return x * f ; } char F[ 200 ] ; inline void write( I_int x ) { if( x == 0 ) { putchar( '0' ) ; return ; } I_int tmp = x > 0 ? x : -x ; if( x < 0 ) putchar( '-' ) ; int cnt = 0 ; while( tmp > 0 ) { F[ cnt ++ ] = tmp % 10 + '0' ; tmp /= 10 ; } while( cnt > 0 ) putchar( F[ -- cnt ] ) ; } #undef I_int } using namespace io ; using namespace std ; #define N 100010 #define M 5000 int block , num ; // block - 块的大小 // num - 块的个数 int a[ N ] ; // a - 原数组 int sum[ M ] , add[ M ] , L[ M ] , R[ M ] , bl[ N ] ; // sum - 区间和 // add - 懒标记 // L - 块左端点 R - 块右端点 bl - 当前点属于哪一个块 int n = read() , m = read() ; void build() { block = sqrt( n ) ; num = n / block ; if( n % block ) num ++ ; for( int i = 1 ; i <= num ; i ++ ) { L[ i ] = (i - 1) * block + 1 ; R[ i ] = i * block ; } R[ num ] = n ; // 有可能有不完整的块 for( int i = 1 ; i <= n ; i ++ ) { bl[ i ] = (i - 1) / block + 1 ; // -1 针对右端点, +1 针对左端点 } for( int k = 1 ; k <= num ; k ++ ) { for( int i = L[ k ] ; i <= R[ k ] ; i ++ ) { sum[ k ] += a[ i ] ; //处理前缀和 } } } void reset( int x ) { // 从新统计当前块的和 sum[ x ] = 0 ; for( int i = L[ x ] ; i <= R[ x ] ; i ++ ) sum[ x ] += a[ i ] ; } void upd( int l , int r , int c ) { if( bl[ l ] == bl[ r ] ) { // 特判 for( int i = l ; i <= r ; i ++ ) a[ i ] += c ; reset( bl[ l ] ) ; return ; } for( int i = l ; i <= R[ bl[ l ] ] ; i ++ ) // 处理散块 a[ i ] += c ; for( int i = L[ bl[ r ] ] ; i <= r ; i ++ ) a[ i ] += c ; reset( bl[ l ] ) ; reset( bl[ r ] ) ; // 处理整块 for( int i = bl[ l ] + 1 ; i < bl[ r ] ; i ++ ) add[ i ] += c ; } int query( int l , int r ) { int ans = 0 ; if( bl[ l ] == bl[ r ] ) { for( int i = l ; i <= r ; i ++ ) ans += a[ i ] + add[ bl[ i ] ] ; return ans ; } for( int i = l ; i <= R[ bl[ l ] ] ; i ++ ) ans += a[ i ] + add[ bl[ i ] ] ; for( int i = L[ bl[ r ] ] ; i <= r ; i ++ ) ans += a[ i ] + add[ bl[ i ] ] ; for( int i = bl[ l ] + 1 ; i < bl[ r ] ; i ++ ) ans += sum[ i ] + add[ i ] * (R[ i ] - L[ i ] + 1) ; return ans ; } signed main() { for( int i = 1 ; i <= n ; i ++ ) a[ i ] = read() ; build() ; for( int i = 1 ; i <= m ; i ++ ) { int opt = read() , x = read() , y = read() , k ; if( opt == 1 ) { k = read() ; upd( x , y , k ) ; } else outn( query( x , y ) ) ; } return 0 ; }
在讲应用以前插播一个东西
树状数组和线段树的效率均为\(O(nlogn)\),树状数组常数较小,分块效率为\(O(n\sqrt{n})\)
通常树状数组常数优秀的话能够承受到1e6的数据范围
线段树能够承受5e5的数据范围
分块能够承受5e4的数据范围,常数优秀的话能够承受1e5的数据范围
树状数组最难理解,代码实现最简单
线段树较易理解,代码实现最复杂,常数较之树状数组会比较大
分块易理解,代码实现难度适中,复杂度较高
因此请根据实际状况选择不一样的算法
最懒的取法:\(\sqrt{n}\)
最正规的取法:用均值不等式来推
最玄学的取法:在\(\sqrt{n}\)/均值不等式所推出来的大小上下浮动,可能会取出更优
的块的大小
好的取值能够帮助你卡更多的分(这点在后面蒲公英那道题很明显的体现了出来)
而后不知道均值不等式怎么推?
在\(\sqrt{n}\)上下浮动,是上仍是下根据实际状况:
对于询问比修改多的操做,向上浮动
对于修改比询问多的操做,向下浮动
但其实正常状况下(即大部分题目)\(\sqrt{n}\)就够能够了,少部分题目才须要推块的大小来卡常(以及你的分块暴力若是想拿高分也能够推一下块的大小)
几种应用将以例题形式呈现
区间加,查询一个区间中大于等于k的数的个数
N<=1e6
按理说没办法过,可是事实上跑的挺快的
将块内元素排序。
修改时使用懒标记,对于散块暴力修改而后从新排序
能够作到\(\sqrt{n}\)修改
如何查询?
由于每一个块是互不影响的。因此咱们能够对每一个块二分查找第一个大于等于它的数的下标,区间右端点减去该下标即为该区间对答案的贡献。
散块依旧暴力查询
查询复杂度为\(O(\sqrt{n}*log2(\sqrt{n}))\)
#include <bits/stdc++.h> using namespace std; #define N 1000100 int n,m,a[N]; int block,num,l[N],r[N],belong[N],sum[N],add[N]; void build(){ block=sqrt(n); num=n/block; if(n%block)num++; for(int i=1;i<=num;i++){ l[i]=block*(i-1)+1; r[i]=block*i; } r[num]=n; for(int i=1;i<=n;i++){ belong[i]=(i-1)/block+1; sum[i]=a[i]; } for(int i=1;i<=num;i++){ sort(sum+l[i],sum+r[i]+1); } } void copy(int x){ for(int i=l[x];i<=r[x];i++){ sum[i]=a[i]; } sort(sum+l[x],sum+r[x]+1); } void upd(int L,int R,int c){ if(belong[L]==belong[R]){ for(int i=L;i<=R;i++){ a[i]+=c; } copy(belong[L]); return; } for(int i=L;i<=r[belong[L]];i++)a[i]+=c; copy(belong[L]); for(int i=l[belong[R]];i<=R;i++)a[i]+=c; copy(belong[R]); for(int i=belong[L]+1;i<=belong[R]-1;i++)add[i]+=c; } int find(int L,int R,int c){ int r1=R; while(L<=R){ int mid=(L+R)>>1; if(sum[mid]<c)L=mid+1; else R=mid-1; } return r1-L+1; } int query(int L,int R,int c){ int ans=0; if(belong[L]==belong[R]){ for(int i=L;i<=R;i++){ if(a[i]+add[belong[i]]>=c)ans++; } return ans; } for(int i=L;i<=r[belong[L]];i++){ if(a[i]+add[belong[i]]>=c)ans++; } for(int i=l[belong[R]];i<=R;i++){ if(a[i]+add[belong[i]]>=c)ans++; } for(int i=belong[L]+1;i<=belong[R]-1;i++){ ans+=find(l[i],r[i],c-add[i]); } return ans; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]); build(); for(int i=1;i<=m;i++){ char ch[10]; int L,R,c; scanf("%s%d%d%d",ch,&L,&R,&c); if(ch[0]=='M')upd(L,R,c); else printf("%d\n",query(L,R,c)); } return 0; }
对每一个点点处理出跳出当前块要跳多少次,跳出当前块以后在哪一个地方。
由于是单点修改,因此直接修改整个块内的每一个点就好,效率\(O(\sqrt{n})\)
对于查询,直接从当前点开始跳,只须要跳\(\sqrt{n}\)次,因此也是\(O(\sqrt{n})\)
#include <cstdio> #include <cmath> #include <algorithm> #include <cstring> #define ll long long #define N 200010 inline void read(int &x){ x=0;int f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+c-'0';c=getchar();} x*=f; } using namespace std; int n,a[N],m; int block,num,to[N],d[N],l[N],r[N],belong[N]; void build(){ block=sqrt(n),num=n/block; if(n%block)num++; for(int i=1;i<=num;i++){ l[i]=(i-1)*(block)+1; r[i]=block*i; } r[num]=n; for(int i=1;i<=n;i++){ belong[i]=(i-1)/block+1; } for(int i=n;i;i--){ if(belong[i+a[i]]!=belong[i]){ d[i]=1; to[i]=i+a[i]; }else { d[i]=d[i+a[i]]+1; to[i]=to[i+a[i]]; } } } void upd(int x,int c){ a[x]=c; for(int i=r[belong[x]];i>=l[belong[x]];i--){ if(belong[i+a[i]]!=belong[i]){ to[i]=i+a[i]; d[i]=1; }else { d[i]=d[i+a[i]]+1; to[i]=to[i+a[i]]; } } } int query(int x){ int ans=0; while(x<=n){ ans+=d[x]; x=to[x]; } return ans; } int main(){ read(n); for(int i=1;i<=n;i++)read(a[i]); build(); read(m); while(m--){ int x,y; read(x);read(y); if(x==1)printf("%d\n",query(y+1)); else { int k;read(k); upd(y+1,k); } } }
对每一个点预处理出该点颜色的上一次在哪里出现,设为pre。
那么在一个区间里面,颜色i第一次出现即意味着pre_i<l(l为区间左端点)
因此咱们能够套用教主的魔法那题的套路,对pre进行排序,查询时在块内二分查找获得该块对答案的贡献。复杂度\(O(\sqrt{n}*log2(\sqrt{n}))\)
可是这题不同的是修改操做,这道题的修改须要O(n)的时间来修改(须要把整个的pre数组都给改了)
由于BZOJ保证了修改的操做<=1000因此这题就能够用分块水了
正解是带修莫队。
luogu增强了数据这种分块写法只能水40分
#include <bits/stdc++.h> using namespace std; inline void read( int &x ){ x = 0 ; int f = 1 ; char c = getchar() ; while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; } while( c >= '0' && c <= '9' ) { x = (x << 1) + (x << 3) + c - 48 ; c = getchar() ; } x *= f ; } #define N 1000100 int belong[N],block,num,pre[N],last[N]; int n,a[N],m,b[N]; void reset(int x){ int l=(x-1)*block+1,r=min(n,block*x); for(int i=l;i<=r;i++)pre[i]=b[i]; sort(pre+l,pre+r+1); } void build(){ block=int(sqrt(n)+log(2*n)/log(2)); num=n/block; if(n%block)num++; for(int i=1;i<=n;i++){ b[i]=last[a[i]]; belong[i]=(i-1)/block+1; last[a[i]]=i; } for(int i=1;i<=num;i++)reset(i); } int find(int i,int x){ int lt=(i-1)*block+1,l=lt,r=min(i*block,n); while(l<=r){ int mid=(l+r)>>1; if(pre[mid]<x)l=mid+1; else r=mid-1; } return l-lt; } int query(int l,int r){ int ans=0; if(belong[l]==belong[r]){ for(int i=l;i<=r;i++){ if(b[i]<l)ans++; } return ans; } for(int i=l;i<=belong[l]*block;i++){ if(b[i]<l)ans++; } for(int i=(belong[r]-1)*block+1;i<=r;i++){ if(b[i]<l)ans++; } for(int i=belong[l]+1;i<belong[r];i++){ ans+=find(i,l); } return ans; } void upd(int l,int x){ for(int i=1;i<=n;i++)last[a[i]]=0; a[l]=x; for(int i=1;i<=n;i++){ int lt=b[i]; b[i]=last[a[i]]; if(lt!=b[i])reset(belong[i]); last[a[i]]=i; } } int main(){ read( n ) ; read( m ) ; for(int i=1;i<=n;i++)read( a[i] ) ; build(); for(int i=1;i<=m;i++){ int l,r; char ch[10]; scanf("%s",ch); read( l ) ; read( r ) ; if(ch[0]=='Q')printf("%d\n",query(l,r)); else upd(l,r); } return 0; }
在线区间众数,经典分块题
作法不少,这里提供一种\(O(n*\sqrt{n}+n*\sqrt{n}*log2(\sqrt{n})\)的作法
首先数的值域为1e9确定要离散化一下,由于数最多有40000个因此开40000个vector,存一下每一个数出现的位置
预处理出每一个以块的端点为左右端点的区间的众数,这种区间一共有\(O(block^2)\)个,因此能够用\(O(n*block)\)的时间复杂度来预处理
能够发现的一点是,每一个区间的众数,要么是散块里面的数,要么是中间全部整块的区间众数(由于散块中出现的那些数增长了中间的整块中第二大第三大的这些区间众数的出现次数,他们就有可能篡位了)
那么咱们能够在离散化以后,将每一个数出现的位置存到一个vector里面,在处理散块中的数的时候,咱们能够经过二分查找找出这个区间中该数出现过几回(二分查找右端点和左端点相减),效率是\(O(\sqrt{n}*log2(\sqrt{n}))\)
整块直接调用咱们预处理出来的区间众数就能够了
块的大小能够推一下均值不等式,听说在30~200之间比较好,30最快,我在洛谷上面块的大小用200跑了9000ms用30跑了3000ms,中间的数据也试过几个,都没有30的表现好(这是开了O2的,不开O2的话200跑不过去,30跑13000ms)
#include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f #define il inline namespace io { #define in(a) a=read() #define out(a) write(a) #define outn(a) out(a),putchar('\n') #define I_int int inline ll read() { ll x = 0 , f = 1 ; char c = getchar() ; while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; } while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; } return x * f ; } char F[ 200 ] ; inline void write( I_int x ) { if( x == 0 ) { putchar( '0' ) ; return ; } I_int tmp = x > 0 ? x : -x ; if( x < 0 ) putchar( '-' ) ; int cnt = 0 ; while( tmp > 0 ) { F[ cnt ++ ] = tmp % 10 + '0' ; tmp /= 10 ; } while( cnt > 0 ) putchar( F[ -- cnt ] ) ; } #undef I_int } using namespace io ; using namespace std ; #define N 100010 map< int , int > mp ; vector< int > vt[ N ] ; int val[ N ] , a[ N ] ; int t[ 5010 ][ 5010 ] ; int n , tot = 0 ; int block , num , bl[ N ] , L[ N ] , R[ N ] ; int cnt[ N ] ; void pre( int x ) { int mx = 0 , id = 0 ; memset( cnt , 0 , sizeof( cnt ) ) ; for( int i = L[ x ] ; i <= n ; i ++ ) { cnt[ a[ i ] ] ++ ; if( cnt[ a[ i ] ] > mx || (cnt[ a[ i ] ] == mx && val[ a[ i ] ] < val[ id ] ) ) { mx = cnt[ a[ i ] ] ; id = a[ i ] ; } t[ x ][ bl[ i ] ] = id ; } } void build() { block = 30 ; num = n / block ; if( n % block ) num ++ ; for( int i = 1 ; i <= num ; i ++ ) { L[ i ] = (i - 1) * block + 1 ; R[ i ] = i * block ; } R[ num ] = n ; for( int i = 1 ; i <= n ; i ++ ) bl[ i ] = (i - 1) / block + 1 ; for( int i = 1 ; i <= num ; i ++ ) pre( i ) ; } int serach_ans( int l , int r , int x ) { return upper_bound( vt[ x ].begin() , vt[ x ].end() , r ) - lower_bound( vt[ x ].begin() , vt[ x ].end() , l ) ; } int query( int l , int r ) { int mx = 0 , id = t[ bl[ l ] + 1 ][ bl[ r ] - 1 ] ; mx = serach_ans( l , r , id ) ; if( bl[ l ] == bl[ r ] ) { for( int i = l ; i <= r ; i ++ ) { int x = serach_ans( l , r , a[ i ] ) ; if( x > mx || (x == mx && val[ a[ i ] ] < val[ id ])) { mx = x ; id = a[ i ] ; } } return id ; } for( int i = l ; i <= R[ bl[ l ] ] ; i ++ ) { int x = serach_ans( l , r , a[ i ] ) ; if( x > mx || (x == mx && val[ a[ i ] ] < val[ id ])) { mx = x ; id = a[ i ] ; } } for( int i = L[ bl[ r ] ] ; i <= r ; i ++ ) { int x = serach_ans( l , r , a[ i ] ) ; if( x > mx || (x == mx && val[ a[ i ] ] < val[ id ])) { mx = x ; id = a[ i ] ; } } return id ; } int main() { n = read() ; int m = read() ; int ans = 0 ; for( int i = 1 ; i <= n ; i ++ ) { a[ i ] = read() ; if( mp[ a[ i ] ] == 0 ) { mp[ a[ i ] ] = ++ tot , val[ tot ] = a[ i ] ; } a[ i ] = mp[ a[ i ] ] ; vt[ a[ i ] ].push_back( i ) ; } build() ; for( int i = 1 ; i <= m ; i ++ ) { int l = read() , r = read() ; l = (l + ans - 1) % n + 1 , r = (r + ans - 1) % n + 1 ; if( l > r ) swap( l , r ) ; outn( ans = val[ query( l , r ) ] ) ; } return 0 ; }
分块在不少题目中是以非正解的形式出现的
可是确实对于水分它是一个很好的算法
(若是你的常数足够优秀的话甚至能够吊打正解)
(如弹飞绵羊分块吊打lct)
能够去写一下hzwer的数列分块入门
而后若是不怕死的话能够去写一下lxl的毒瘤分块题