0x40 數據結構進階

0x41 並查集

定義

在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集的合併及查詢問題。有一個聯合-查找算法定義了兩個用於此數據結構的操做:node

  • \(Find\):肯定元素屬於哪個子集。它能夠被用來肯定兩個元素是否屬於同一子集。
  • \(Union\):將兩個子集合併成同一個集合。

因爲支持這兩種操做,一個不相交集也常被稱爲聯合-查找數據結構或合併-查找集合。其餘的重要方法,\(MakeSet\),用於建立單元素集合。有了這些方法,許多經典的劃分問題能夠被解決。c++

爲了更加精確的定義這些方法,須要定義如何表示集合。一種經常使用的策略是爲每一個集合選定一個固定的元素,稱爲表明,以表示整個集合。接着,\(Find(x)\) 返回 \(x\) 所屬集合的表明,而 $Union $使用兩個集合的表明做爲參數。算法

路徑壓縮與按秩合併

這是兩個並查集經常使用的優化數組

當咱們在尋找祖先時,一旦元素多且來,並查集就會退化成單次\(O(n)\)的算法,爲了解決這一問題咱們能夠在尋找祖先的過程當中直接將子節點連在祖先上,這樣能夠大大下降複雜度,均攤複雜度是\(O(log(n))\)數據結構

按秩合併也是常見的優化方法,「秩」的定義很普遍,舉個例子,在不路徑壓縮的狀況下,常見的狀況是把子樹的深度定義爲秩app

不管如何定義一般狀況是把「秩」儲存在根節點,合併的過程當中把秩小的根節點插到根大的根節點上,這樣能夠減小操做的次數函數

特別的,若是把秩定義爲集合的大小,那麼採用了按秩合併的並查集又稱「啓發式並查集」測試

按秩合併的均攤複雜度是\(O(log(n))\)的,若是同時採用按秩合併和路徑壓縮均攤複雜度是\(O(\alpha(n) )\),\(\alpha(n)\)是反阿克曼函數
\[ \forall n \le 2^{10^{19729}},\alpha(n)\le 5 \]
能夠視爲均攤複雜度爲\(O(1)\)優化

不過一般狀況下咱們僅採用路徑壓縮便可ui

int father[N];

int getfather( int x )//查詢
{
    if( father[x] == x ) return x;
    return father[x] = getfather( father[x] );
}

inline void union( int x , int y )//合併
{
    register int fx = getfather( x ) , fy = getfather( y );
    father[ fx ] = fy;
    return ;
}

inline bool same( int x  , int y ) { return getfather( x ) == getfather( y ) ;}
//判讀是否在同一結合

//把深度看成秩的 按秩合併
memset( rank , 0 , sizeof( rank ) );
inline void rank_union( int x , int y )
{
    fx = getfather( x ) , fy = getfather( y );
    if( rank[ fx ] < rank[ fy ] ) ) father[ fx ] = fy;
    else 
    {
        father[ fy ] = fx;
        if( rank[ fx ] == rank[ fy ] ) rank[ fx ] ++;
    }
    return ;
}

NOI2015 程序自動分析

雖然是\(noi\)的題但整體仍是很簡單的,本質就是維護一個並查集

根據操縱把相同的所有合併,在把逐一判斷不相同的

爲何不能反過來作呢?舉個例子\(a\ne b,b\ne c\)可否推出\(a\ne c\)呢?

顯然不能

爲何?由於不等關係沒有傳遞性

那爲何相同能夠呢?由於相同是有傳遞性的

因此從本題也可知並查集維護的必定要具備傳遞性

那麼剩下的就數據比較大,但\(n\le 1e6​\)因此離散化便可

#include <bits/stdc++.h>
#define PII pair< int , int >
#define S second 
#define F first 
using namespace std;


const int N = 1e6+5;
int n , cur[ N * 2 ] , fa[ N * 2 ] , ta , tb , cnt;
PII a[N] , b[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;   
}

inline int getfa( int x )
{
    if( x == fa[x] ) return x;
    return fa[x] = getfa( fa[x] );
}

inline void work()
{
    cnt = ta = tb = 0;
    n = read();
    for( register int i = 1 , u , v , w ; i <= n ; i ++ )
    {
        u = read() , v = read() , w = read();
        cur[ ++ cnt ] = u , cur[ ++ cnt ] = v;
        if( w ) a[ ++ ta ] = { u , v };
        else b[ ++ tb ] = { u , v };
    }
    sort( cur + 1 , cur + 1 + cnt );
    cnt = unique( cur + 1 , cur + 1 + cnt ) - cur - 1;
    for( register int i = 1 ; i <= cnt ; i ++ ) fa[i] = i;
    register int fx , fy;
    for( register int i = 1 ; i <= ta ; i ++ )
    {
        a[ i ].F = lower_bound( cur + 1 , cur + 1 + cnt , a[i].F ) - cur;
        a[ i ].S = lower_bound( cur + 1 , cur + 1 + cnt , a[i].S ) - cur;
        fx = getfa( a[i].F ) , fy = getfa( a[i].S );
        fa[ fx ] = fy;
    }
    for( register int i = 1 ; i <= tb ; i ++ )
    {
        b[ i ].F = lower_bound( cur + 1 , cur + 1 + cnt , b[i].F ) - cur;
        b[ i ].S = lower_bound( cur + 1 , cur + 1 + cnt , b[i].S ) - cur;
        
        fx = getfa( b[i].F ) , fy = getfa( b[i].S );
        if(  fx !=  fy  ) continue;
        puts("NO");
        return ;
    }
    puts("YES");
    return ;
}

int main()
{
    for( register int T = read() ; T ; T -- ) work(); 
    return 0;
}

帶權並查集

咱們在維護並查集實際的過程當中額外的在維護一個\(dis\)表明從當前的點到根節點的距離。因爲路徑壓縮致使每次訪問後都會將因此的點指向根節點。因此咱們要在每次維護的過程當中更新\(dis\)數組,此時須要咱們在維護一個\(size\)數組,表明在每一個根節點的子樹的大小,怎樣咱們合併的過程當中就能夠把根節點\(x\)插到根節點\(y\)的後面,而且讓\(dis[x]+=size[y]\)。這樣咱們就能夠在壓縮路徑的過程當中,不斷的更新每一個節點到根節點的距離

注意這裏的狀況說的是,每一個樹都是一條鏈,且每次都是將一條連接在另外一條鏈的後面

那麼若是是將任意一顆子樹插到另外一顆子樹的任意一個節點怎麼辦辦呢?

而且我還要壓縮路徑,實際上是能夠的

咱們而且只用兩個數組\(father\)\(dis\)就能夠實現

int father[N] , dis[N]

inline int getfather( int x ) 
{
    if( father[x] == x ) return x;
    register int root =getfather( father[x] );
    dis[x] = dis[ father[x] ] + 1;
    return father[x] = root;
}

inline void merge( int x , int y )//把 根節點x 插到 結點y 上
{
    register int fx = getfather( x ) , fy = getfather( y );
    fa[x] = fy; dis[fx] += dis[y] + 1 ;
    return ;
}

inline void init()//初始化
{
    for( register int i = 1 ; i <= n ; i++ ) father[i] = i;
}

注意這裏的\(x\)必須是根節點

假設咱們要把\(x\)插到\(y\)上,咱們直接用\(dis[y]+1\)來更新\(dis[x]\),對於\(x\)的子節點咱們能夠在遞歸的時候修改

注意,若是須要使用\(dis[x]\)在用以前必需要先調用一次\(getfather(x)\),來更新一下\(dis[x]\)\(fahter[x]\)

因此時間複雜度可能會略高,但沒有具體證實,由於這個算法是一天中午我本身琢磨出來的,且沒有在網上找嚴格的證實

NOI2002 銀河英雄傳說

這就是到帶權並查集的模板,因此在沒有在上面放代碼

能夠直接琢磨下這個代碼

#include <bits/stdc++.h>
using namespace std;


const int N = 30005;
int fa[N] , dis[N] ,size[N] , n ;
char opt;

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline int getfa( int x )
{
    if( fa[x] == x ) return x;
    register int root = getfa( fa[x] );
    dis[ x ] += dis[ fa[x] ];
    return fa[x] = root;
}

inline void merge( int x , int  y )
{
    x = getfa( x ) , y = getfa( y );
    fa[ x ] = y, dis[ x ] += size[ y ] , size[y] += size[x] , size[ x ] = 0;
    return ;
}

inline int query( int x , int y )
{
    register int fx = getfa( x ) , fy = getfa( y );
    if( fx != fy ) return - 1;
    return abs( dis[x] - dis[y] ) - 1;
}

int main()
{
    n = read();
    for( register int i = 1 ;  i < N ; i ++ ) fa[i] = i , size[i] = 1 ;
    for( register int u , v ; n ; n -- )
    {
        do{ opt = getchar() ; }while( opt != 'C' && opt != 'M' );
        u = read() , v = read() ;
        if(opt == 'M') merge( u , v );
        else printf( "%d\n" , query( u , v ) ); 
    }
    return 0;
}

擴展域並查集

擴展域並查集就是將並查集的區域大小擴展成整數倍,用多個區域來同時維護多個傳遞關係

AcWing 239. 奇偶遊戲

這是一道經典的擴展域並查集

首先咱們用一個\(sum[x]\)數組,表明從\(1\)\(x\)\(1\)的個數

若是當前詢問的答案是\(even\)也就是偶數,那麼\(sum[l-1]\)\(sum[r]\)的寄偶性應該相同

若是當前詢問的答案是\(odd\)也就是寄數,那麼\(sum[l-1]\)\(sum[r]\)的寄偶性應該不相同

因此咱們能夠建一個大小爲\(2n\)的並查集,其中\(1\cdots n\)表示偶數的關係、\(n+1\cdots 2n\)表示奇數

爲了表示方便咱們定義兩個變量\(x\_even=x,x\_odd=x+n\)

若是奇數的話咱們就把\(x\_even\)\(y\_odd\)合併,\(x\_odd\)\(y\_even\)合併

若是偶數的話咱們就把\(x\_odd\)\(y\_odd\)合併,\(x\_even\)\(y\_even\)合併

另外在每次合併前都要判斷一下時候正確

#include <bits/stdc++.h>
#define PII pair< int , int >
#define PIIB pair < PII , bool > 
#define F first
#define S second
#define hash( x ) ( lower_bound( a + 1 , a + 1 + n , x ) - a )
using namespace std;


const int N = 10010;
int a[ N << 1 ] , n , m , fa[ N << 2 ];
PIIB opt[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline int getfa( int x )
{
    if( fa[x] == x ) return x;
    return fa[x] = getfa( fa[x] );
}

int main()
{
    n = read() , m = read() , n = 0;
    register string str;
    for( register int i = 1 ; i <= m ; i ++ ) 
    {
        opt[i].F.F = read() , opt[i].F.S = read();
        a[ ++ n ] = opt[i].F.F - 1 , a[ ++ n ] = opt[i].F.S;
        cin >> str;
        if( str == "even" ) opt[i].S = 1;
        else opt[i].S = 0;
    }
    //離散化 
    sort( a + 1 , a + 1 + n );
    n = unique( a + 1 , a + 1 + n ) - a - 1 ;
    for( register int i = 1 ; i <= m ; i ++ ) opt[i].F.F = hash( opt[i].F.F - 1 ) , opt[i].F.S = hash( opt[i].F.S );
    
    for( register int i = 1 ; i <= 2 * n ; i ++ ) fa[i] = i;
    
    
    for( register int i = 1 , x_even , x_odd , y_even , y_odd ; i <= m ; i ++ )
    {
        x_even = getfa( opt[i].F.F + n ) , x_odd = getfa( opt[i].F.F ) , y_even = getfa( opt[i].F.S + n ) , y_odd = getfa( opt[i].F.S );
        if( opt[i].S ) // 不一樣 
        {
            if( x_odd == y_even ) printf( "%d\n" , i - 1) , exit(0);
            fa[ x_even ] = y_even , fa[ x_odd ] = y_odd;
        }
        else// 相同
        {
            if( x_even == y_even ) printf( "%d\n" , i - 1 ) , exit(0);
            fa[ x_even ] = y_odd , fa[ x_odd ] = y_even;
        }
    }
    printf( "%d\n" , m );
    return 0;
}

NOI2001 食物鏈

經典的擴展域並查集,咱們能夠開三個域,同類,食物,天敵,來維護這樣一個集合

一樣爲了方便表示,分別用\(x_a,x_b,x_c\)表示\(x\)的同類,\(x\)的食物,\(x\)的天敵

若是\(x\)\(y\)是同類,就把\(x_a\)\(y_a\)合併、\(x_b\)\(y_b\)合併、\(x_c\)\(y_c\)合併

若是\(x​\)\(y​\),就把\(x_a​\)\(y_c​\)合併、\(x_b​\)\(y_a\)合併、\(x_c​\)\(y_b​\)合併

因此針對每次操做前想判斷是否出現衝突,在進行合併便可

#include <bits/stdc++.h>
using namespace std;


const int N = 5e4 + 5;
int n , m , cnt , fa[ N * 3 ];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline int getfa( int x )
{
    if( fa[x] == x ) return x;
    return fa[x] = getfa( fa[x] );
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= n * 3 ; i ++ ) fa[i] = i;
    for( register int opt , x , y , x_a , x_b , x_c , y_a , y_b , y_c ; m >= 1 ; m -- )
    {
        opt = read() , x = read() , y = read();
        x_a = getfa( x ) , x_b = getfa( x + n ) , x_c = getfa( x + 2 * n ) , y_a = getfa( y ) , y_b = getfa( y + n ) , y_c = getfa( y + 2 * n );
        // x_a x的同類 x_b x的食物 x_c x的天敵 
        if( x > n || y > n || ( opt == 2 && x == y ) ) { cnt ++ ; continue; }
        if( opt == 1 )
        {
            if( x_b == y_a || x_c == y_a ) { cnt ++ ; continue ; }
            fa[ x_a ] = y_a , fa[ x_b ] = y_b , fa[ x_c ] = y_c;
        }
        else
        {
            if( x_a == y_a || x_c == y_a ) { cnt ++ ; continue ; }
            fa[ x_a ] = y_c , fa[ x_b ] = y_a , fa[ x_c ] = y_b;
        }
    }
    cout << cnt << endl;
    return 0;
}

NOIP2017 奶酪

這道題是\(NOIP2017\)來的一道題,這道題也是我第一次參加聯賽遇到題,考場上我並無看出這是道並查集

這道題其實很暴力由於\(n\)的範圍比較小,我麼能夠直接\(O(N^2)\)暴力枚舉,而後用並查集判斷便可,在隨後枚舉與上下底面相交或相切的圓判斷是否在一個集合裏便可

#include <bits/stdc++.h>
#define LL long long
#define PII pair< LL , LL >
#define PIII pair < PII , LL >
#define F first
#define S second
#define pb( x ) push_back( x )
#define square( x ) ( x * x )
using namespace std;


const int N = 1005;
LL n , h , r , fa[N] ;
vector < LL > Floor , Roof;
vector < PIII > node;


inline LL read()
{
    register LL x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

inline LL getfa( LL x )
{
    if( x == fa[x] ) return x;
    return fa[x] = getfa( fa[x] );
}

inline void merge( LL x , LL y )
{
    x = getfa( x ) , y = getfa( y );
    fa[x] = y;
    return ;
}

inline bool pending( LL x , LL y ) { return ( square( ( node[x].F.F - node[y].F.F ) ) + square( ( node[x].F.S - node[y].F.S ) ) + square( ( node[x].S - node[y].S ) ) ) <= r * r * 4 ; }

inline void work()
{
    n = read() , h = read() , r = read() , node.clear() , Floor.clear() , Roof.clear();
    for( register int i = 0 , x , y , z ; i < n ; i ++ )
    {
        fa[i] = i , x = read() , y = read() , z = read();
        node.push_back( { { x , y } , z } );
        if( z - r <= 0 ) Floor.push_back( i );
        if( z + r >= h ) Roof.push_back( i );
    }
    for( register int i = 0 ; i < n ; i ++ )
    {
        for( register int j = i + 1 ; j < n ; j ++ )
        {
            if( pending( i , j ) ) merge( i , j );
        }
    }
    for( auto i : Floor )
    {
        for( auto j : Roof )
        {
            if( getfa( i ) != getfa( j ) ) continue;
            puts("Yes");
            return ;
        }
    }
    puts("No");
    return ;
}


int main()
{
    for( register int T = read() ; T >= 1 ; T -- ) work();
    return 0;
}

0x42 樹狀數組

樹狀數組這裏就不講原理了,給張圖本身理解便可

注意樹狀數組維護的數組下標必須是\(1\cdots n\),若是有\(0\)就會死循環

Loj 130. 樹狀數組 1 :單點修改,區間查詢

模板題直接看代碼便可

#include <bits/stdc++.h>
#define lowbit( x ) ( x & -x )
#define LL long long
using namespace std;


const int N = 1e6 + 5;
LL n , m , bit[N];


inline LL read()
{
    register LL x = 0 , f = 1 ;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f ;
}

inline void add( LL x , LL w )
{
    for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[i] += w;
}

inline LL find( LL x )
{
    register LL sum = 0;
    for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i];
    return sum;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 , x ; i <= n ; i ++ ) x = read() , add( i , x );
    for( register int i = 1 , opt , x , y ; i <= m ; i ++ )
    {
        opt = read() , x = read() , y = read();
        if( opt == 1 ) add( x , y );
        else printf( "%lld\n" , find( y ) - find( x - 1 ) );
    }
    return 0;
}

Loj 10116. 清點人數

模板題,沒啥好解釋的,以前看模板就能寫出來

Loj 131. 樹狀數組 2 :區間修改,單點查詢

區間修改也是樹狀數組的經典操做,簡單來講就是維護一個差分序列

#include <bits/stdc++.h>
#define LL long long
#define lowbit( x ) ( x & - x )
using namespace std;


const int N = 1e6 + 1000;
LL n , m , bit[N];


inline LL read()
{
    register LL x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

inline LL add( LL x , LL w )
{
    for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[i] += w ;
}

inline LL find( LL x )
{
    register LL sum = 0;
    for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i];
    return sum;
}


int main()
{
    n = read() , m = read();
    for( register LL i = 1 , last = 0 , x ; i <= n ; i ++ ) x =read() ,add( i , x - last ) , last = x ;
    for( register LL i = 1 , opt , l , r , w ; i <= m ; i ++ )
    {
        opt = read();
        if( opt == 1 )
        {
            l = read() , r = read() , w = read();
            add( l , w ) , add( r + 1 , -w );
        }
        else printf("%lld\n" , find( read() ) );
    }
    return 0;
}

Loj 10117.簡單題

仍是到模板題,直接套板子吧

Loj 132. 樹狀數組 3 :區間修改,區間查詢

聯繫上一道題,咱們假設原數組是\(a[i]\)維護一個差分數組\(d[i]\)天然能夠獲得
\[ \sum_{i=1}^{n}a[i]=\sum_{i=1}^{n}\sum_{j=1}^{i}d[i] \]
而後咱們發現\(d[1]\)用了\(p\)次,\(d[2]\)用了\(p-1\)次,\(d[i]\)用了\(p-i+1\)次,因此能夠獲得
\[ \sum_{i=1}^{n}\sum_{j=1}^{i}d[i]=\sum_{i=1}^{n}d[i]\times(n-i+1)=(n+1)\times\sum_{i=1}^{n}d[i]\times\sum_{i=1}^{n}(d[i]\times i) \]
因此咱們能夠同時維護兩個數組\(sum1[i]=\sum d[i],sum2[i]=\sum (d[i]\times i)\)

查詢

查詢位置\(p\)\((p+1)\)乘以\(sum1\)\(p\)的前綴減去\(sum2\)\(p\)的前綴

查詢\([l,r]\)的區間和,\(r\)的前綴減\(l-1\)的前綴

修改

對於\(sum1\)中的修改相似與上一個問題的修改

對於\(sum2\)中的修改,給\(sum2[l]+=l\times x , sum2[r+1]-=(r+1)\times x\)

#include <bits/stdc++.h>
#define LL long long
#define lowbit( x ) ( x & - x )
using namespace std;

const int N = 1e6 +5;
int n , m ;
LL sum1[N] , sum2[N];


inline int read()
{
    register int x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

inline void add( int x , LL w )
{
    for( register int i = x ; i <= n ; i += lowbit( i ) ) sum1[i] += w , sum2[i] += w * x;
}

inline LL find( int x )
{
    register LL s = 0 , t = 0 ;
    for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) s += sum1[i] , t += sum2[i];
    return ( x + 1 ) * s - t;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 , x , last = 0 ; i <= n ; i ++ ) x = read() , add( i , x - last ) , last = x ;

    for( register int i = 1 , opt , l , r , w ; i <= m ; i ++ )
    {
        opt = read();
        if( opt == 1 ) l = read() , r = read() , w = read() , add( l , w ) , add( r + 1 , - w );
        else l = read() , r = read() , printf( "%lld\n" , find( r ) - find( l - 1 ) );
    }
    return 0;
}

Loj 10114.數星星 Stars

根據樹狀數組的性質咱們能夠快速的求出前\(k\)個數的和,加上座標是遞增給的,咱們能夠在每次插入前統計在當前星星以前有多少個星星便可,顯然縱座標是沒有用的

注意座標是從\((0,0)\)開始的,但樹狀數組的下標是從\(1\)開始因此給座標總體加\(1\)便可p

#include <bits/stdc++.h>
#define lowbit( x ) ( x & - x )
using namespace std;


const int N = 15e3 + 5 , M =  32010;
int n , bit[M] , level[N];


inline int read()
{
    register int x = 0 , f = 1 ;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f ;
}

inline void add( int x , int w )
{
    for( register int i = x ; i <= 32001 ; bit[i] += w , i += lowbit( i ) );
}

inline int find( int x )
{
    register int sum = 0;
    for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i];
    return sum;
}

int main()
{
    n = read();
    for( register int i = 1 , x ; i <= n ; x = read() + 1 , read() , level[ find(x) ] ++ , add( x , 1 ) , i ++ );
    for( register int i = 0 ; i < n ; printf( "%d\n" , level[i] ) , i ++ );
    return 0;
}

Loj 10115.校門外的樹

咱們把每次種樹抽象成一個線段,同時開兩個樹狀數組分別維護每條線段的兩個端點

插入時在\(l\)處增長一個左端點,\(r\)處增長一個右端點

查詢時查詢$1\cdots r $的右端點個數,\(1\cdots l\)的左端點個數,作差就是\(l\cdots r\)中線段的個數

#include <bits/stdc++.h>
#define lowbit( x ) ( x & - x )
using namespace std;


const int N = 5e4 + 5;
int n , m , bit[2][N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while ( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void add( int x , int w , int k )
{
    for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[k][i] += w ;
}

inline int find( int x , int k )
{
    register int res = 0 ;
    for( register int i = x ; i >= 1 ;  i -= lowbit( i ) ) res += bit[k][i];
    return res;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 , op , l , r ; i <= m ; i ++ )
    {
        op = read() , l = read() , r = read();
        if( op == 1 ) add( l , 1 , 0 ) , add( r , 1 , 1  );
        else printf( "%d\n" , find( r , 0 ) - find( l - 1  , 1 ) );
    }
    return 0;
}

Luogu P1908 逆序對

逆序對是樹狀數組的經典操做,其實至關於用樹狀數組維護了一個桶

#include <bits/stdc++.h>
#define lowbit( x ) ( x & - x )
#define LL long long
using namespace std;


const int N = 5e5 + 5 ;
int n , m , a[N] , b[N] , bit[N];
LL cnt ;

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void add( int x , int w )
{
    for( register int i = x ; i <= n ; bit[i] += w , i += lowbit( i ) );
}

inline LL find( int x )
{
    register LL sum = 0;
    for( register int i = x ; i >= 1 ; sum += bit[i] , i -= lowbit( i ) );
    return sum;
}

int main()
{
    n = read();
    for( register int i = 1 ; i <= n ; i ++ ) a[i] = b[i] = read();
    sort( b + 1 , b + 1 + n );
    m = unique( b + 1 , b + 1 + n ) - b - 1;
    for( register int i = 1 ; i <= n ; i ++ ) a[i] = lower_bound( b + 1 , b + 1 + m , a[i] ) - b;
    for( register int i = n ; i >= 1 ; i -- )
    {
        add( a[i] , 1 );
        cnt += find( a[i] - 1 );
    }
    cout << cnt << endl;
    return 0;
}

Loj 3195. 「eJOI2019」異或橙子

首先先來兩個引理
\[ a \oplus a = 0\\0\oplus a = a \]
知道這個引理後,咱們就能夠看下樣例
\[ a_2 \oplus a_3 \oplus a_4 \oplus (a_2 \oplus a_3) \oplus (a_3 \oplus a_4) \oplus (a_2 \oplus a_3 \oplus a_4)=a_2 \oplus a_2 \oplus a_2 \oplus a_3 \oplus a_3 \oplus a_3\oplus a_3 \oplus a_4 \oplus a_4 \oplus a_4\\=a_2\oplus0 \oplus a_4= 2 \]
也就是說咱們能夠根據異或的一些性質,來優化一下

若是\(a\)的個數是奇數個其貢獻就是\(a\),若是\(a\)的個數是偶數個其貢獻就是\(0\)

手推幾個數據就能發現

若是\(l,r\)奇偶性不一樣全部的數都是偶數個,結果天然是\(0\)

若是\(l,r\)奇偶性相同,那麼只有和\(l,r\)奇偶性的位置上的數纔會有貢獻

咱們能夠開兩個樹狀數組來維護下,一個維護奇數位上的異或前綴和,另外一個維護偶數位上的異或前綴和

對於操做1,注意不是把第\(i\)位異或\(j\)是把\(i\)爲修改成\(j\),根據異或和的性質咱們要先異或\(a[i]\)在異或\(j\),因此能夠異或$a[i]\oplus j $

對於操做2首先特判奇偶性不一樣的,對於奇偶性相同的,咱們在對應的樹狀數組裏求出\(r,l-1\)的異或前綴和,在異或一下便可

#include <bits/stdc++.h>
#define lowbit( x ) ( x & - x )
using namespace std;


const int N = 2e5 + 10;
int n , m , bit[2][N] , a[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void add( int val , int pos , int k )
{
    for( register int i  = pos ; i <= n ; bit[k][i] ^= val , i += lowbit( i ) );
}

inline int find( int pos , int k )
{
    register int res = 0;
    for( register int i = pos ; i >= 1 ; res ^= bit[k][i] , i -= lowbit( i ) );
    return res;
}

int main()
{
    n = read() , m = read();
    for( register int i = 1  ; i <= n ; a[i] = read() , add( a[i] , i , i & 1 ) ,  i ++ );
    for( register int i = 1 , op , l , r ; i <= m ; i ++ )
    {
        op = read() , l = read() , r = read();
        if( op == 1 ) add( a[l] ^ r , l , l & 1 ) , a[l] = r ;
        else printf( "%d\n" , ( ( l + r ) & 1 ) ? 0 : ( find( r , r & 1 ) ^ find( l - 1 , r & 1 ) ) );
    }
    return 0;
}

Luogu P1168 中位數

考慮如何用樹狀數組作

咱們能夠依次插入每一個數

好比咱們要插入\(x\),就個\(x\)個這個位置加\(1\),而後\(find(x)\)求前綴和,就知道小於等於\(x​\)的數有多少個

而後\(A_i\)的範圍很大,\(n\)的範圍比較小,且咱們不須要知道每一個數的具體大小,只需知道相對大小便可,天然選擇離散化

而後就是求第\(k\)個數有多大,如過二分的話是\(O(log^2(n))​\)的,比較慢

根據樹狀數組的特性,考慮倍增,具體過程至關把lowbit的過程倒過來,具體能夠看代碼理解

#include <bits/stdc++.h>
#define lowbit( x ) ( x & -x )
using namespace std;


const int N = 1e5 + 5;
int n , m , a[N] , b[N] , bit[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
}  

inline void add( int x , int p )
{
    for( register int i = x ; i <= m ; i += lowbit( i ) ) bit[i] += p;
}

inline int find( int k )
{
    register int ans = 0, cnt = 0;// ans 是答案 , cnt 是小於等於 ans 的數有多少個
    for( register int i = 20 ; i >= 0 ; i -- )
    {
        ans += ( 1 << i );
        if( ans > m || cnt + bit[ ans ] >= k ) ans -= ( 1 << i );
        else cnt += bit[ ans ];
    }
    return ans + 1;
}

int main()
{
    m = n = read();
    for( register int i = 1 ; i <= n ; i ++ ) a[i] = b[i] = read();
    sort( a + 1 , a + 1 + n );
    m = unique( a + 1 , a + 1 + m) - a - 1;//去重
    for( register int i = 1 ; i <= n ; i ++ ) b[i] = lower_bound( a + 1 , a + 1 + m , b[i] ) - a; //離散化
    for( register int i = 1 ; i <= n ; i ++ )
    {
        add( b[i] , 1 );
        if( i & 1 ) printf( "%d\n" , a[find( ( i + 1 ) >> 1 )] );
    }
    return 0;
}

0x43 線段樹

線段樹(英語:\(Segment\ tree\))是一種二叉樹形數據結構,\(1977\)年由\(Jon Louis Bentley\)發明,用以存儲區間或線段,而且容許快速查詢結構內包含某一點的全部區間。

一個包含 \({\displaystyle n}\)個區間的線段樹,空間複雜度爲 \({\displaystyle O(n)}\),查詢的時間複雜度則爲 ${\displaystyle O(\log n+k)} $,其中 \({\displaystyle k}\)是匹配條件的區間數量。

此數據結構亦可推廣到高維度

建樹

struct Node
{
    int l , r , value , tag;
    Node * left , * right; // 左右子樹
    Node (int s , int t , int v ,  Node * a , Node * b)
    {
        l = s; r = t; value = v; tag = 0;
        left = a; right = b;
    }
} * root; //根節點

Node * build(int l,int r)
{
    if(l == r) return new Node( l , r , a[l] , 0 , 0 );//葉子節點
    register int mid = ( l + r ) >> 1;
    Node * left = build( l , mid), * right = build( mid+1 , r ); // 遞歸構建左右子樹
    return new Node( l , r , left -> value + right -> value , left , right); // 創建當前節點
}

單點修改,區間查詢

int find( int l , int r , Node * cur)
{
    if(l == cur -> l && r == cur -> r) return cur -> value; // 徹底包含
    int mid = ( cur -> l + cur -> r ) >> 1;
    if( l > mid ) return find( l , r, cur -> right); // 所有在右子樹
    if(mid >= r) return find( l , r , cur -> left); // 所有在左子樹
    return find( l , mid , cur -> left) + find( mid + 1 , r , cur -> right ); // 區間跨越mid
}

void modify( int x , int v , Node * cur)
{
    if(cur -> l == cur -> r ) cur -> value += v; // 葉子節點
    else 
    {
        int mid = (cur -> l + cur -> r ) >> 1;
        modify(x , v , x > mid ? cur -> right : cur -> left);
        cur -> value = cur -> left -> value + cur -> right -> value;
    }
}

區間修改,單點查詢

單點修改只要將\(l == r\)便可,因此很少作介紹

區間修改,區間查詢

區間快速修改有兩種方法

  • 延遲標記
  • 標記永久化(不會)

考慮在每一個節點維護一個標記tag,並執行如下操做

  1. 若是在修改過程當中當前節點被徹底包含在修改區間內,給區間打上修改標記,並馬上回溯
  2. 當要查詢或修改當前節點的子樹時,將標記下放的到子樹
inline void mark(int v,Node * cur)
{
    cur -> tag += v;
    cur -> value += (cur -> r - cur -> l + 1) * v;
    return ;
}

inline void pushdown( Node * cur)
{
    if(cur -> tag == 0) return ;
    
    if(cur -> left)
    {
        mark(cur -> tag,cur -> left);
        mark(cur -> tag,cur -> right);
    }
    else cur -> value += cur -> tag;
    cur -> tag = 0;
    return ;
}

inline int query( int l , int r , Node * cur)
{
    if(l <= cur -> l &&  cur -> r <= r  ) return cur -> value;
    register int mid = (cur -> l + cur -> r) >> 1 , res = 0;
    pushdown( cur );
    if( l <= mid ) res +=  query( l , r , cur -> left );
    if( mid + 1 <= r) res +=  query( l , r , cur -> right);
    return res;
}

void extent_modify( int l , int r , int v , Node * cur) // [l,r] + v 
{
    
    if(cur -> l > r || cur -> r < l) return ;
    if(l <= cur -> l && cur -> r <= r)
    {
        mark(v,cur);
        return ;
    }
    pushdown( cur );
    register int mid = (cur -> l + cur -> r) >> 1;
    if(l <= mid) extent_modify( l , r , v , cur -> left);
    if(mid + 1 <= r) extent_modify( l , r , v , cur -> right);
    cur -> value  = cur -> left -> value + cur -> right -> value;
    return ;
}

Luogu SP1716 GSS3 - Can you answer these queries III

與基礎的線段樹操做很像,咱們額外的維護三個值\(dat,ldat,rdat\)分別表明整個區間的最大子段和、當前區間從作左端點開始的最大子段和,從右端點開始的最大子段和

考慮如何更新當前結點

inline void update( Node * cur )
{
    cur -> sum = cur -> left -> sum + cur -> right -> sum;
    //更新sum
    cur -> ldat = max( cur -> left -> ldat , cur -> left -> sum + cur -> right -> ldat );
    //從左起的最大子段多是 左區間的最大子段 或 左區間的和加右區間的最大子段
    cur -> rdat = max( cur -> right -> rdat , cur -> right -> sum + cur -> left -> rdat );
    //相似上面
    cur -> dat = max( max( cur -> left -> dat , cur -> right -> dat ) , 
        max( max( cur -> ldat , cur -> rdat ) , cur -> left -> rdat + cur -> right -> ldat ) ) ;
    //當前區間的的最大子段和要麼是 左右區間的的最大子段和,要麼是中間的最大子段和,要麼是左右端點開始的最大子段和
}

也就是說線段是不止能維護區間和,實際上只要是知足結合律的均可以用線段樹來維護,區間和,區間最值,區間異或和等

#include <bits/stdc++.h>
using namespace std;


const int N = 500005 , INF = 0x7f7f7f7f;
int n , a[N] ;
struct Node
{
    int l , r , sum , dat , ldat , rdat ;
    Node * right , * left;
    Node( int  a , int b , int c , int d , int e , int f , Node * g , Node * h ) { l = a , r = b , sum = c , dat = d , ldat = e , rdat = r , left = g , right = h ; }
} * root ;


inline int read()
{
    register int x = 0 , f = 1 ;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = - 1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

inline void update( Node * cur )
{
    cur -> sum = cur -> left -> sum + cur -> right -> sum;
    cur -> ldat = max( cur -> left -> ldat , cur -> left -> sum + cur -> right -> ldat );
    cur -> rdat = max( cur -> right -> rdat , cur -> right -> sum + cur -> left -> rdat );
    cur -> dat = max( max( cur -> left -> dat , cur -> right -> dat ) , max( max( cur -> ldat , cur -> rdat ) , cur -> left -> rdat + cur -> right -> ldat ) ) ;
}

inline Node * build( int l , int r  )
{
    Node * cur = new Node( l , r , 0 , 0 , 0 , 0 , NULL , NULL );
    if( l == r )
    {
        cur -> ldat = cur -> rdat = cur -> sum = cur -> dat = a[l];
        return cur ;
    }
    register int mid = ( l + r ) >> 1;
    cur -> left = build( l , mid );
    cur -> right = build( mid + 1 , r);
    update( cur );
    return cur;
}

inline Node * query( int l , int r , Node * cur )
{
    if( l <= cur -> l && cur -> r <= r ) return cur;
    register int mid = ( cur-> l + cur -> r ) >> 1;
    if( r <= mid ) return query( l , r , cur -> left );
    if( l > mid ) return query( l , r , cur -> right );
    Node *res = new Node( l , r , 0 , 0 , 0 , 0 , 0 , 0 );
    Node * L = query( l , r , cur -> left ) , * R = query( l , r , cur -> right );
    res -> sum = L -> sum + R -> sum;
    res -> ldat = max( L -> ldat , L -> sum + R -> ldat );
    res -> rdat = max( R-> rdat , R -> sum + L -> rdat );
    res -> dat = max( max( L -> dat , R -> dat ) , max( max( res -> ldat , res -> rdat ) , L -> rdat + R -> ldat ) );
    return res;
}

inline void change( int x , int w , Node * cur )
{
    if( cur -> r == x && x == cur -> l )
    {
        cur -> sum = cur -> dat = cur ->ldat = cur -> rdat = w;
        return ;
    }
    register int mid = ( cur -> r + cur -> l ) >> 1;
    if( x <= mid ) change( x , w , cur -> left );
    if( x > mid ) change( x , w  , cur -> right );
    update( cur );
    return ;
}


int main()
{
    n = read() ;
    for( register int i = 1 ; i <= n ; i ++ ) a[i] = read();
    root = build( 1 , n );
    for( register int  m = read() , op , x , y ; m >= 1 ; m --  )
    {
        op = read() , x = read() , y = read();
        if( op ) printf( "%d\n" , query( x , y , root ) -> dat );
        else change( x , y ,root );
    }
    return 0;
}

Luogu P4939 Agent2

這道題用了樹狀數組的經常使用操做,說白了就是區間修改,單點查詢

這道題須要實現的的功能線段樹也能夠,可是用過代碼對比和實際測試,線段樹過不了,而且代碼很長

因此經過這道題能夠得知,若是能夠用樹狀數組的話就不要用線段樹

樹狀數組

#include <bits/stdc++.h>
#define lowbit( x ) ( x & -x )
using namespace std;


const int N = 1e7 + 5;
int n , m , l , r , op , bit[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void add( int x , int v )
{
    for( register int i = x ; i <= n ; i += lowbit(i) ) bit[i] += v;
    return ;
}

inline int find( int x )
{
    register int sum = 0;
    for( register int i = x ; i  ; i -= lowbit(i) ) sum += bit[i];
    return sum;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= m ; i ++ )
    {
        op = read();
        if( op ) printf( "%d\n" , find( read() ) );
        else add( read() , 1 ) , add( read() + 1 , -1 );
    }
    return 0;
}

線段樹

#include <bits/stdc++.h>
using namespace std;

int n , m ;

struct Node
{
    int l , r , value , tag;
    Node * left , * right;
    Node( int s , int t , int  w , Node * a , Node * b )
    {
        l = s , r = t , value = w , tag = 0;
        left = a , right = b;
    }
} *root;

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline Node * build( int l , int r )
{
    if( l == r ) return new Node( l , r , 0 , 0 , 0 );
    register int mid = ( l + r ) >> 1;
    Node * left = build( l , mid ) , * right = build( mid + 1 , r );
    return new Node( l , r , 0 , left , right );
}

inline void mark( int w , Node * cur ) { cur -> tag += w , cur -> value += ( cur -> r - cur -> l + 1 ) * w; }

inline void pushdown( Node * cur )
{
    if( cur -> tag == 0 ) return ;
    if( cur -> left ) mark( cur -> tag , cur -> left ) , mark( cur -> tag , cur -> right );
    else cur -> value += cur -> tag;
    cur -> tag = 0;
    return ;
}

inline int query( int l , int r , Node * cur )
{
    if( l <= cur -> l && cur -> r <= r ) return cur -> value;
    register int mid = ( cur -> l + cur -> r ) >> 1 , res = 0;
    pushdown( cur );
    if( l <= mid ) res += query( l , r , cur -> left );
    if( mid + 1 <= r ) res += query( l , r , cur -> right );
    return res;
}

inline void modify( int l , int r , int w , Node * cur )
{
    if( cur -> l > r || cur -> r < l) return ;
    if( l <= cur-> l && cur -> r <= r )
    {
        mark( w , cur );
        return ;
    }
    register int mid = ( cur -> l + cur -> r ) >> 1;
    if( l <= mid ) modify( l , r , w , cur -> left );
    if( mid + 1 <= r ) modify( l , r , w , cur -> right);
    cur -> value = cur -> left -> value + cur -> right -> value;
    return ;
}

int main()
{
    n = read() , m = read();
    root = build( 1 , n );
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int op = read();
        if( !op )
        {
            register int x = read() , y = read();
            modify( x , y , 1 , root );
        }
        else 
        {
            register int x = read();
            printf( "%d\n" , query( x , x  , root ) );
        }
    }
    return 0;
}

0x44 分塊

本節部份內容選自 oi-wiki

簡介

其實,分塊是一種思想,而不是一種數據結構。

從 NOIP 到 NOI 到 IOI,各類難度的分塊思想都有出現。

一般的分塊算法的複雜度帶根號,或者其餘奇怪的複雜度,而不是 \(\log\)

分塊是一種很靈活的思想,幾乎什麼都能分塊,而且不難實現。

你想寫出什麼數據結構就有什麼,缺點是漸進意義的複雜度不夠好。

固然,在 \(n=10^5\) 時,因爲常數小,跟線段樹可能差很少。

這不是建議大家用分塊的意思,在 OI 中,能夠做爲一個備用方案,首選確定是線段樹等高級的數據結構。

如下經過幾個例子來介紹~

區間和

動機:線段樹太難寫?

將序列分段,每段長度 \(T\) ,那麼一共有 \(\frac{n}{T}\) 段。

維護每一段的區間和。

單點修改:顯然。

區間詢問:會涉及一些完整的段,和最多兩個段的一部分。

完整段使用維護的信息,一部分暴力求。

複雜度 \(O(\frac{n}{T}+T)\)

區間修改:一樣涉及這些東西,使用打標記和暴力修改,一樣的複雜度。

\(T=\sqrt{n}\) 時,複雜度 \(O(\sqrt{n})\)

區間和 2

上一個作法的複雜度是 \(\Omega(1) , O(\sqrt{n})\)

咱們在這裏介紹一種 \(O(\sqrt{n}) - O(1)\) 的算法。

爲了 \(O(1)\) 詢問,咱們能夠維護各類前綴和。

然而在有修改的狀況下,不方便維護,只能維護單個塊內的前綴和。

以及整塊做爲一個單位的前綴和。

每次修改 \(O(T+\frac{n}{T})\)

詢問:涉及三部分,每部分均可以直接經過前綴和獲得,時間複雜度 \(O(1)​\)

對詢問分塊

一樣的問題,如今序列長度爲 \(n\) ,有 \(m\) 個操做。

若是操做數量比較少,咱們能夠把操做記下來,在詢問的時候加上這些操做的影響。

假設最多記錄 \(T\) 個操做,則修改 \(O(1)\) ,詢問 \(O(T)\)

\(T\) 個操做以後,從新計算前綴和, \(O(n)\)

總複雜度: \(O(mT+n\frac{m}{T})​\)

\(T=\sqrt{n}\) 時,總複雜度 \(O(m \sqrt{n})\)

Loj 6277. 數列分塊入門 1

咱們用以個相似懶惰標記的東西來維護,對於整塊咱們只修改標記,對於散塊暴力修改便可

#include <bits/stdc++.h>
using namespace std;


const int N = 50005 , M = 250;
int n , len , tot , a[N] , tag[M] , pos[N] , lef[M] , rig[M];


inline int read()
{
    register int x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

inline void add( int l , int r , int val )
{
    if( pos[l] == pos[r] )
    {
        for( register int i = l ; i <= r ; a[i] += val , i ++ );
        return ;
    }
    for( register int i = l ; i <= rig[ pos[l] ] ; a[i] += val , i ++ );
    for( register int i = r ; i >= lef[ pos[r] ] ; a[i] += val , i -- );
    for( register int i = pos[l] + 1 ; i <= pos[r] - 1 ; tag[i] += val , i ++ );
    return ;
}


int main()
{
    n = read() , len = sqrt( 1.0 * n ) , tot = n / len + ( n % len ? 1 : 0 );
    for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , i ++ );
    for( register int i = 1 ; i <= tot ; lef[i] = ( i - 1 ) * len + 1 , rig[i] = i * len , i ++ );
    for( register int i = 1 , opt , l , r , val ; i <= n ; i ++ )
    {
        opt = read() , l = read() , r = read() , val = read();
        if( opt ) printf( "%d\n" , a[r] + tag[ pos[r] ] );
        else add( l , r , val );
    }
    return 0;
}

Loj 6278. 數列分塊入門 2

開一個vector儲存每個塊內的元素,而後排序

若是修改包含當前整個塊在不會改變塊內元素的相對大小,只修改標記

若是沒有可以包含整個塊,就暴力修改,而後從新排序便可

查詢時,對於散塊直接暴力掃一遍,整塊的話二分查找

#include <bits/stdc++.h>
#define L( x ) ( ( x - 1 ) * len + 1 )
#define R( x ) ( x * len )
#define pb( x ) push_back( x )
using namespace std;


const int N = 50005 , M = 250;
int n , a[N] , len , tag[M] , pos[N];
vector< int > group[M];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void reset( int x )
{
    group[x].clear();
    for( register int i = L( x ) ; i <= R( x ) ; i ++ ) group[x].pb( a[i] );
    sort( group[x].begin() , group[x].end() );
    return ;
}

int query(int l,int r,int val)
{
    register int res = 0 , p = pos[l] , q = pos[r];
    if( p == q )
    {
        for( register int i = l ; i <= r ; i ++ )
        {
            if( a[i] + tag[p] < val ) res ++;
        }
        return res;
    }
    for( register int i = l ; i <= R( p ) ; i ++ )
    {
        if( a[i] + tag[p] < val ) res ++;
    }
    for( register int i = r ; i >= L( q ) ; i -- )
    {
        if( a[i] + tag[q] <  val ) res ++;
    }
    for( register int i = p + 1 ; i <= q - 1 ; i ++ ) res += lower_bound( group[i].begin() , group[i].end() , val - tag[i] ) - group[i].begin();
    return res;
}


inline void modify( int l , int r , int val )
{
    register int p = pos[l] , q = pos[r];
    if( p == q )
    {
        for( register int i = l ; i <= r ; a[i] += val , i ++ );
        reset( p );
        return ;
    }
    for( register int i = l ; i <= R( p ) ; a[i] += val , i ++ );
    for( register int i = r ; i >= L( q ) ; a[i] += val , i -- );
    reset( p ) , reset( q );
    for( register int i = p + 1 ; i <= q - 1 ; tag[i] += val , i ++ );
    return ;
}


int main()
{
    n = read() , len = sqrt( 1.0 * n );
    for( register int i = 1 ; i <= n ; a[i] = read()  , group[ pos[i] = ( i - 1 ) / len + 1 ].pb( a[i] ) , i ++ );
    for( register int i = 1 ; i <= pos[n] ; sort( group[i].begin() , group[i].end() ) , i ++ );
    for( register int i = 1 , opt , l , r , val  ; i <= n ; i ++ )
    {
        opt = read() , l = read() , r = read() , val = read();
        if( opt ) printf( "%d\n" , query( l , r , val * val ) );
        else modify( l , r , val );
    }
    return 0;
}

經過這兩道,咱們能夠以總結下怎麼思考一個分塊

  1. 不完整的塊怎麼處理
  2. 完整的塊怎麼處理
  3. 預處理什麼信息

Loj 6279. 數列分塊入門 3

這道題的作法和第二題比較相似

分塊,塊內排序,二分查找,塊外暴力掃

對於這道題你會發現若是直接把分紅\(\sqrt{n}\)塊的話會\(T\)掉一部分點

因此咱們這這裏引入均值不等式\(\sqrt{xy} \le \frac{1}{2}(x+y)\),當且僅當\(x=y\)時,\(\sqrt{xy} = \frac{1}{2}(x+y)\)

假設序列長度爲\(n\),塊的大小爲\(x\),天然有\(y=\frac{n}{x}\)塊,假設每次操做的複雜度爲\(Ax+By\)

則根據均值不等式能夠獲得$Ax+By \ge 2\sqrt{ABn} $

因此可知
\[ Ax=By\Rightarrow x=\frac{By}{A}=\frac{Bn}{Ax}\Rightarrow x^2=\frac{B}{A}n\Rightarrow x=\sqrt{\frac{B}{A}n} \]
根據上面的推到可知當\(x=\sqrt{\frac{B}{A}n}\)時複雜度最低,最低爲\(O(2\sqrt{ABn})\)

對於本題每次操做是\(O(x+log_ny)\)的,天然可得\(x=\sqrt{nlog_n}\approx 700\)

可是因爲咱們在計算複雜度時只考慮數量級,不免會有偏差

因此咱們能夠用兩個僅僅時塊的大小不同的程序對拍,比較時間找出比較快的分塊大小

#include <bits/stdc++.h>
#define L( x ) ( ( x - 1 ) * len + 1 )
#define R( x ) ( x * len )
using namespace std;


const int N = 1e5+5 , M = 1e3 + 5 ,INF = - 1 << 31;
int n , len , a[N] , pos[N] , tag[M];
set< int > group[M];


inline int read()
{
    register int x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}


inline int query( int l , int r , int val )
{
    register int res = -1 , p = pos[l] , q = pos[r];
    if( p == q )
    {
        for( register int i = l ; i <= r ; i ++ )
        {
            if( a[i] + tag[ q ] < val ) res = max( res , a[i] + tag[ q ] );
        }
        return ( res != INF ? res : - 1 );
    }
    for( register int i = l ; i <= R( p ) ; i ++ )
    {
        if( a[i] + tag[ p ] < val ) res = max( res , a[i] + tag[p] );
    }
    for( register int i = r ; i >= L( q ) ; i -- )
    {
        if( a[i] + tag[ q ] < val ) res = max( res , a[i] + tag[q] );
    }
    for( register int i = p + 1 ; i <= q - 1 ; i ++ )
    {
        auto t = group[i].lower_bound( val - tag[i] );
      if( t == group[i].begin() ) continue;
      t -- ;
        res = max( res , *t + tag[i] );
    }
    return res;
}

inline void modify( int l , int r , int val )
{
    register int p = pos[l] , q = pos[r];
    if( p == q )
    {
        for( register int i = l ; i <= r ; group[p].erase(a[i]) , a[i] += val , group[p].insert(a[i]) , i ++ ) ;
        return ;
    }
    for( register int i = l ; i <= R( p ) ; group[p].erase(a[i]) , a[i] += val , group[p].insert(a[i]) , i ++ );
    for( register int i = r ; i >= L( q ) ; group[q].erase(a[i]) , a[i] += val , group[q].insert(a[i]) , i -- );

    for( register int i = p + 1 ; i <= q - 1 ; tag[i] += val , i ++ );
    return ;
}


int main()
{
    n = read() , len = 1000;
    for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , group[ pos[i] ].insert( a[i] ) , i ++ );
    for( register int i = 1 , opt , l , r , val ; i <= n ; i ++ )
    {
        opt = read() , l = read() , r = read() , val = read();
        if( opt )printf("%d\n" , query( l , r , val ) );
        else  modify( l , r , val );
    }
    return 0;
}

P3372 【模板】線段樹 1

沒錯,這是一道線段樹模板題,可是不妨來用線段樹作一下是能夠的

#include <bits/stdc++.h>
#define LL long long
#define L( x ) ( ( x - 1 ) * len + 1 )
#define R( x ) ( x * len )
using namespace std;


const int N = 1e5 + 5 , M = 1e3 + 5 ;
int n , m , len , tot , pos[N] , lef[M] , rig[M];
LL a[N] , sum[N] , tag[M];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline LL query( int l , int r )
{
    register LL res = 0;
    if( pos[l] == pos[r] )//若是在一個塊中
    {
        for( register int i = l ; i <= r ; res += a[i] + tag[ pos[i] ] , i ++ );
        return res;
    }
    for( register int i = l ; i <= rig[ pos[l] ] ; res += a[i] + tag[ pos[i] ] , i ++ );//散塊
    for( register int i = r ; i >= lef[ pos[r] ] ; res += a[i] + tag[ pos[i] ] , i -- );
    for( register int i = pos[l] + 1 ; i <= pos[r] - 1 ; res += sum[i] + tag[i] * ( rig[i] - lef[i] + 1 ) , i ++ );//整塊
    return res;
}


inline void modify( int l , int r , LL val )
{
    for( register int i = l ; i <= min( r , rig[ pos[l] ] ) ; a[i] += val , sum[ pos[i] ] += val , i ++ );//散塊
    for( register int i = r ; i >= max( l , lef[ pos[r] ] ) ; a[i] += val , sum[ pos[r] ] += val , i -- );
    for( register int i = pos[l] + 1  ; i <= pos[r] - 1 ; tag[i] += val , i ++ );//整塊
}


int main()
{
    n = read() , m = read();
    len = sqrt( 1.0 * n ) , tot = n / len; if( n % len ) tot ++;

    for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , sum[ pos[i] ] += a[i] , i ++)
    for( register int i  = 1 ; i <= tot ; lef[i] = ( i - 1 ) * len + 1 , rig[i] = i * len , i ++ );

    for( register int opt , l , r , val ; m >= 1 ; m -- )
    {
        opt = read() , l = read() , r = read();
        if( opt & 1 ) val = read() , modify( l , r , val );
        else printf( "%lld\n" , query( l , r ) );
    }
    return 0;
}

請忽視我分塊開了\(O2\),能夠注意到,分塊不只代碼短並且跑得並不慢

不過這道題貌似沒有構造數據

相關文章
相關標籤/搜索