1. 程式人生 > >樹狀數組 線段樹

樹狀數組 線段樹

編號 math 曾經 延遲 原來 道理 用途 延遲標記 read

樹狀數組

樹狀數組的基本用途是維護序列的前綴和,相比前綴和數組,樹狀數組優勢在於高效率的單點修改,單點增加(前綴和數組單點修改效率比較低)

因為樹狀數組的思想,原理還是很好理解的,就直接講基本算法;

1 lowbit函數

關於lowbit這個函數,可能會有點難以理解,但其實你不理解也沒關系,把模板背下來就好

根據任意正整數關於2的不重復次冪的唯一分解性質,例如十進制21用二進制表示為10101,其中等於1的位是第0,2,4(最右端是第0位)位,即21被二進制分解成\(2^4+2^2+2^0\);

進一步地,整個區間[1,21]可以分成如下3個小區間:

長度為\(2^4\)的小區間[1,\(2^4\)];

長度為\(2^2\)的小區間[\(2^4+1\),\(2^4+2^2\)];

長度為\(2^0\)的小區間[\(2^4+2^2+1\),\(2^4+2^2+2^0\)];

對於給定的初始序列A,我們可以建立一個數組c,c[x]表示序列A的區間[x-lowbit(x)+1,x)]中所有數的和;

int lowbit(int x){return x&-x;}

2 單點增加操作

void update(int x,int y){
    for(;x<=n;x+=lowbit(x))
        c[x]+=y;
}

3 查詢前綴和

int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

4 擴展

上述查詢前綴和是統計[1,x]的前綴和,若要統計區間[x,y]的和,則調用sum函數即可:sum(y)-sum(x-1);

多維樹狀數組:

(擴充為m維)將原來的修改和查詢函數中的一個循環,改成m個循環m維數組c中的操作;

\(n*m\)的二維數組為例:

將(x,y)的值加上z,不是把區間[x,y]中的每個值加z
int update(int x,int y,int z){
    int i=x;
    while(i<=n){
        int j=y;
        while(j<=m){
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lowbit(i);
    }
}

int sum(int x,int y){
    int res=0,i=x;
    while(i>0){
        int j=y;
        while(j>0){
            res+=c[i][j];
            j-=lowbit(j);
        }
        i-=lowbit(i);
    }
    return res;
}

註意樹狀數組的下標絕對不能為0,因為lowbit(0)=0,這樣會陷入死循環

兩道模板題,多打打模板~~

https://www.luogu.org/problemnew/show/P3374
https://www.luogu.org/problemnew/show/P3368

線段樹

線段樹是一種基於分治思想的二叉樹結構,比樹狀數組更加通用,下文總結會比較這兩種數據結構;

線段樹的基本用途是對序列進行維護,支持查詢與修改指令;

線段樹的每個節點都代表一個區間;

線段樹具有唯一的根節點,代表的是整個區間,即[1,n];

線段樹的每個葉節點都代表一個長度為1的區間,即[x,x];

對於每個內部節點[l,r],它的左子節點是[l,mid],右子節點是[mid+1,r],其中mid=(l+r)/2(向下取整);(子節點與父節點的性質近似於二叉堆的結構);

**保存的數組長度要不小於4*n;**

1 建樹:線段樹的二叉樹結構可以很方便地從下往上傳遞信息;

下面以區間最大值為例:

struct TREE{
    int l,r;
    int dat;
}t[n*4];
//一般會用結構體存儲線段樹

void build(int p,int l,int r){
    t[p].l=l;t[p].r=r;//節點p代表區間[l,r]
    if(l==r){t[p].dat=a[l];return;}//葉節點
    int mid=(l+r)/2;
    build(p*2,l,mid);//左子節點
    build(p*2+1,mid+1,r);//右子節點
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
    //從下往上傳遞信息
}

build(1,1,n);//調用入口

2 單點修改:線段樹中,根節點(編號為1的節點)是執行各種指令的入口;上述建樹過程中的調用入口的第一個1就是這個道理;

我們還是以區間最大值問題為例

void change(int p,int x,int v){
    if(t[p].l==t[p].r){t[p].dat=v;return;}
    //找到葉節點
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,v);
    else change(p*2+1,x,v);
    //判斷x屬於哪邊區間
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}

change(1,x,v);

3 區間查詢

以查詢區間最大值為例:

若[l,r]完全覆蓋了當前節點代表的區間,則立即回溯,並且該節點的dat值為候選答案

若左子節點與[l,r]有重疊部分,則遞歸訪問左子節點;

若右子節點與[l,r]有重疊部分,則遞歸訪問右子節點;

(這裏自己稍微理解一下就懂了,或者自己畫個圖)

假設我們查詢區間[2,7]的最大值,現在有一個節點的代表區間為[3,5],那麽屬於完全覆蓋的情況,return,並且該節點的dat值為候選答案;

現在還有[2,3]和[5,7]這兩個區間沒有處理,我們繼續尋找節點,如果有一個節點與該區間有交集,即有重疊的區間,我們就繼續向下訪問該節點,直至找到一個節點完全被[2,3]或[5,7]覆蓋,那麽跟上面一樣處理即可;(記住我們是從上往下訪問線段樹,所以區間範圍從上往下是越來越小的)

int ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r)return t[p].dat;
    int mid=(t[p].l+t[p].r)/2;
    int val=-(1<<30);
    if(l<=mid) val=max(val,ask(p*2,l,r));
    if(r>mid) val=max(val,ask(p*2+1,l,r));
    return val;
}

ask(1,l,r);

延遲標記:標識該節點曾經被修改,但其子節點尚未被修改(即一個節點被打上延遲標記的同時,它自身保存的信息應該已經被修改完畢)

通俗地講,我們在進行區間增加操作時,如果去更改區間中的每個數(即遍歷到每個葉節點),時間復雜度會增加到O(N);

試想一下,有可能我們修改了該區間,但所有的查詢中與該區間沒有關系(即該區間沒有對我們的答案產生貢獻),相當於我們做了一次無用的操作;

於是就可以用到延遲標記,我們在執行修改指令時,給節點p一個標記,標識該節點曾經被修改,但其子節點尚未被修改,如果在後續的查詢指令中,我們需要知道p節點的子節點的信息,如果p節點有標記,那麽更新p的兩個子節點,同時給p的兩個子節點打上延遲標記,然後清除p的標記;

以區間增加問題為例:

void spread(int p){
    if(t[p].add){//如果節點P有標記
 t[p*2].sum+=t[p].add*(t[p*2].r-t[p*2].l+1);
    //更新左子節點信息
t[p*2+1].sum+=t[p].add*(t[p*2+1].r-t[p*2+1].l+1);
    t[p*2].add+=t[p].add;//給左子節點打延遲標記
    t[p*2+1].add+=t[p].add;
    t[p].add=0;//清除p的標記
    }
}

void change(int p,int x,int y,int v){
    if(x<=t[p].l&&y>=t[p].r){//完全覆蓋
        t[p].sum+=v*(t[p].r-t[p].l+1);
        //更新節點信息
        t[p].add+=v;//給節點打上延遲標記
        return;
    }
    spread(p);    //下傳延遲標記
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,y,v);
    if(y>mid) change(p*2+1,x,y,v);
    t[p].sum=t[p*2].sum+t[p*2+1].sum;
}

long long ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r) 
        return t[p].sum;
    spread(p);//下傳延遲標記
    int mid=(t[p].l+t[p].r)/2;
    long long ans=0;
    if(l<=mid) ans+=ask(p*2,l,r);
    if(r>mid) ans+=ask(p*2+1,l,r);
    return ans;
}

最後比較一下樹狀數組和線段樹:

樹狀數組可以實現單點修改和區間求和(如果將序列進行差分,那麽還可以實現區間價值和單點詢問);

線段樹能實現樹狀數組的所有操作,除此之外,還可以通過標記下傳或標記永久化進行區間修改和區間詢問;

但樹狀數組的常數比線段樹小,實現也較為簡單(線段樹真的是隨隨便便100行,看來我還是太蒻了)

總之,能用樹狀數組就用樹狀數組吧,畢竟一般都只能用線段樹;

樹狀數組 線段樹