前言
分塊是一種應用很廣的根號算法
有一個別名為“優雅的暴力”
這篇文章偏向於介紹分塊入門,並且講解了幾種OI中經典的分塊套路
(因為幾道例題我做的時間間隔有點遠,所以可能會有幾種奇奇怪怪的不同的碼風,請強迫症患者謹慎食用)
分塊入門
例題:A Simple Problem with Integers
給一個序列,支持區間加,區間查詢
\(N<=100000\) \(Ai<=1e9\) \(M<=100000\)
M為操作數
(其實就是線段樹1)
線段樹和樹狀數組的板子題
但是這里不講線段樹和樹狀數組的做法
我們回歸本真,思考一下使用暴力解決該題
使用前綴和維護
\(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 ;
}
最后
分塊在很多題目中是以非正解的形式出現的
但是確實對於水分它是一個很好的算法
(如果你的常數足夠優秀的話甚至可以吊打正解)
(如彈飛綿羊分塊吊打lct)
其他題目
可以去寫一下hzwer的數列分塊入門
然后如果不怕死的話可以去寫一下lxl的毒瘤分塊題