分塊入門與分塊的經典應用
前言
分塊是一種應用很廣的根號演算法
有一個別名為“優雅的暴力”
這篇文章偏向於介紹分塊入門,並且講解了幾種OI中經典的分塊套路
(因為幾道例題我做的時間間隔有點遠,所以可能會有幾種奇奇怪怪的不同的碼風,請強迫症患者謹慎食用)
分塊入門
例題:A Simple Problem with Integers
給一個序列,支援區間加,區間查詢
\(N<=100000\) \(Ai<=1e9\) \(M<=100000\)
M為運算元
線段樹和樹狀陣列的板子題
但是這裡不講線段樹和樹狀陣列的做法
我們迴歸本真,思考一下使用暴力解決該題
使用字首和維護
\(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}\) 就夠可以了,少部分題目才需要推塊的大小來卡常(以及你的分塊暴力如果想拿高分也可以推一下塊的大小)
幾種應用將以例題形式呈現
Luogu P2801 教主的魔法
區間加,查詢一個區間中大於等於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; }
BZOJ 2002:彈飛綿羊
對每個點點處理出跳出當前塊要跳多少次,跳出當前塊之後在哪個地方。
因為是單點修改,所以直接修改整個塊內的每個點就好,效率\(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); } } }
BZOJ 2120: 數顏色
對每個點預處理出該點顏色的上一次在哪裡出現,設為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; }
LuoguP4168 [Violet]蒲公英
線上區間眾數,經典分塊題
做法很多,這裡提供一種\(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 ; }
最後
分塊在很多題目中是以非正解的形式出現的
但是確實對於水分它是一個很好的演算法
其他題目
可以去寫一下hzwer的數列分塊入門