線段樹初步

線段樹初步

通過了一個暑假的學習頹廢,本蒟蒻又回來了。今天要介紹的算法是線段樹。關於線段樹這個高級數據結構,咱們會從三個方面(即」什麼是線段樹?「、「怎麼種線段樹?」、「線段樹的用途」)來了解。算法


什麼是線段樹?

線段樹的定義:

線段樹是一種二叉搜索樹,與區間樹類似,它將一個區間劃分爲一些單元區間,每一個單元區間對應線段樹中得一個葉結點。——\(by\)百度百科數組

再詳細一點就是說:數據結構

線段樹是一種基於分治思想的二叉樹結構,用於在區間上進行信息統計。與按照二進制位(\(2\)的次冪)進行區間劃分的樹狀數組相比,線段樹是一種更加通用的結構函數


線段樹的性質:

  • 線段樹的每一個節點都表明一個區間。
  • 線段樹具備惟一的根節點,表明的區間是整個統計範圍,如\([1,N]\)
  • 線段樹的每一個葉節點都表明一個長度爲\(1\)的元區間\([x,x]\)
  • 對於每一個內部節點\([l,r]\),他的左子節點是\([l,mid]\),右子節點是\([mid+1,r]\),其中\(mid=\lfloor(l+r)/2\rfloor\)

這樣一來,咱們就能簡單的使用一個\(struct\)數組來保存線段樹。固然,若是樹的最後一層節點在數組中保存的位置不是連續的,直接空出數組中多餘的位置便可。在理想狀況下,\(N\)個葉節點的滿二叉樹有\(N+N/2+N/4+...+2+1=2N-1\)個節點。由於在上述存儲方式下,最後一層產生了空餘,因此保存線段樹的數組長度要不小於\(4N\)才能保證不會越界學習


怎麼種線段樹?

(搞得像一篇生物學博客)ui

\(Step\space 1\) 建樹:

線段樹的基本用途是對序列進行維護,支持查詢與修改指令。給定一個長度爲\(N\)的序列\(A\),咱們能夠在區間\([1,N]\)上創建一顆線段樹,當節點的左端點等於右端點時,節點\([i,i]\)保存\(A[i]\)的值。線段樹的二叉樹結構能夠很方便的從上到下傳遞信息。以區間最大值問題爲例,記\(dat(l,r)\)等於\(max_{l\leq i\leq r}\{A[i]\}\),顯然\(dat(l,r)=max(dat(,l,mid),dat(mid+1,r))\)spa

下面的代碼創建了一棵線段樹而且在每一個節點上保存了對應區間的最大值:code

struct Tree{
    int l;//存儲該節點的左端點
    int r;//存儲該節點的右端點
    int mid;//存儲該節點左兒子和右兒子的分界線
    int ans;//存儲區間[l,r]的最大值
}tree[maxn*4];//數組開四倍

void build(int l,int r,int p){//表示以區間[l,r]爲根節點,且當前節點的編號爲p創建一棵(子)樹
    tree[p].l=l,tree[p].r=r;
    tree[p].mid=(l+r)>>1;//計算出中間的分界線
    if(l==r){
        tree[p].ans=a[l];//若是當前節點爲葉子節點,該節點的值就爲數列對應位置的值
    }
    build(l,tree[p].mid,p<<1);//構建左子樹
    build(tree[p].mid+1,r,p<<1|1);//構建右子樹
    tree[p].ans=max(tree[p>>1].ans,tree[p>>1|1].ans);//在肯定完左兒子和右兒子的值後,用這兩個的值來更新當前的答案值
}

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

\(Step\space 2\) 線段樹的單點修改:

單點修改是一條形如」\(C\space x\space v\)「的指令,表示把\(A[x]\)的值修改成\(v\)htm

在線段樹中,根節點(編號爲\(1\)的節點)是執行各類指令的入口。咱們須要從根節點出發,遞歸找到表明區間\([x,x]\)的節點,而後從下往上更新\([x,x]\)以及它的全部祖先節點上保存的信息。

下面代碼中的函數執行了修改當前節點權值:

void update(int p,int x,int v){//表示修改到點p,要求將A[x]修改成v
    if(tree[p].l==tree[p].r){
        tree[p].ans=v;
        return ;//若是到了區間[x,x],將這個節點的值修改成v
    }
    if(x<=tree[p].mid){
        update(p<<1,x,v);
    } else{
        update(p<<1|1,x,v);
    }//根據x與分界點的mid的關係肯定要遞歸的區間
    tree[p].ans=max(tree[p<<1].ans,tree[p<<1|1].ans);//在肯定完左兒子和右兒子的值後,用這兩個的值來更新當前的答案值
}

\(Step\space 3\) 線段樹的區間查詢:

區間查詢是一條形如"\(Q\space l\space r\)"的指令,例如查詢序列\(A\)在區間\([l,r]\)上的最大值,即\(max_{l\leq i\leq r}\{A[i]\}\)。咱們只須要從根節點開始,遞歸執行如下過程:

\(1.\)\([l,r]\)徹底覆蓋了當前節點所表示的區間,則當即回溯,而且該節點的\(ans\)值爲候選答案。

\(2.\)若左子節點與\([l,r]\)有重疊部分,則遞歸訪問左子節點。

\(3.\)若右子節點與\([l,r]\)有重疊部分,則遞歸訪問右子節點。

下面代碼中的函數執行了區間查詢:

int query(int p,int l,int r){
    if(l<=tree[p].l&&tree[p].r<=r){
        return t[p].ans;
    }
    int now=-(1<<30);
    if(l<=tree[p].mid){
        now=max(now,query(p<<1,l,r));
    }
    if(r>tree[p].mid){
        now=max(now,query(p<<1|1,l,r));
    }
}

printf("%d",query(1,l,r));

至此,線段樹已經能像\(ST\)算法同樣處理區間最值問題,而且還支持動態修改某個數的值。同時,線段樹也已經能支持樹狀數組單點增長與查詢前綴和的指令,接下來就是更加高級的操做了。

延遲標記:

在此通俗的解釋我理解的\(Lazy\)意思,好比如今須要對\([a,b]\)區間值進行加\(c\)操做,那麼就從根節點\([1,n]\)開始調用\(update\)函數進行操做,若是恰好執行到一個子節點,它的節點標記爲\(p\),這時$tree[p].l == a && tree[p].r == b \(這時咱們能夠一步更新此時\)p\(節點的\)sum[p]\(的值,\)sum[p] += c * (tree[p].r - tree[p].l + 1)\(,注意關鍵的時刻來了,若是此時按照常規的線段樹的\)update\(操做,這時候還應該更新\)p\(子節點的\)sum[]\(值,而\)Lazy\(思想偏偏是暫時不更新\)rt\(子節點的\)sum[]\(值,到此就\)return\(,直到下次須要用到rt子節點的值的時候纔去更新,這樣避免許多可能無用的操做,從而節省時間 。 ——\)yicbs$

線段樹的用途?

線段樹能夠應用與不少狀況,對於一些比較簡單的模擬題目,能夠用線段樹切掉(固然只有大佬會這作);

大部分樹狀數組能夠解決的問題,線段樹都能解決,並且會更快一點;

還有就是區間上符合結合律的(如加法、異或),而且含有修改、查詢等操做的題目。

好了,接下來就是刷題時間了,一塊兒\(AC\)吧!

相關文章
相關標籤/搜索