分塊入門與分塊的經典應用

前言

分塊是一種應用很廣的根號算法c++

有一個別名爲「優雅的暴力」算法

這篇文章偏向於介紹分塊入門,而且講解了幾種OI中經典的分塊套路數組

(由於幾道例題我作的時間間隔有點遠,因此可能會有幾種奇奇怪怪的不一樣的碼風,請強迫症患者謹慎食用)優化

分塊入門

例題:A Simple Problem with Integers

給一個序列,支持區間加,區間查詢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}\)就夠能夠了,少部分題目才須要推塊的大小來卡常(以及你的分塊暴力若是想拿高分也能夠推一下塊的大小)

幾種應用將以例題形式呈現

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的毒瘤分塊題

相關文章
相關標籤/搜索