線段樹


 

總原理:

將[1,n]分解成若干特定的子區間(數量不超過4*n)

用線段樹對“編號連續”的一些點,進行修改或者統計操作,修改和統計的復雜度都是O(log2(n))

用線段樹統計的東西,必須符合區間加法,(也就是說,如果已知左右兩子樹的全部信息,比如要能夠推出父節點);否則,不可能通過分成的子區間來得到[L,R]的統計結果。

一個問題,只要能化成對一些“連續點”的修改和統計問題,基本就可以用線段樹來解決了

 

 

 

注意:區分3個概念:原數組下標,線段樹中的下標和存儲下標。

原數組下標,這里都默認下標從1開始(一般用a數組表示,a [1] , a[2] …… a [n] )

線段樹下標,是指該節點所管理的局部點集區,。一般都用區間 [ l , r ] 表示,即這一點存儲的是  a [l] ~ a [r]  區間內的局部解、局部信息。

存儲下標,是指該元素在總內存池 arr 中實際的存儲位置,一般用 arr [ root ]表示。存儲方式類似於“完全二叉樹”,詳細見下面的圖

 

注意,在元數個數N確定的時候,數字對 [ l , r ] 的值 和 root 的值,是對應的。

每一個arr[root],都有一個對應的、專屬於它自己管理的區間 [ l , r ]

因為對於確定的N,構建的線段樹是唯一確定的

具體落實到計算上,就是 l、r 和 root同步變化,例如:(l , mid , root*2) 、 (mid+1 , r , root*2+1)

 

 

線段樹的解題關鍵:

1:要推出父節點,我需要知道兩個子節點的那些信息?————這個決定了, 每個單位結點的數據結構(需要設置哪些成員變量),如何設置node

2:在1的基礎上,已知兩個子節點的完整信息,我該如何推出父節點?————定義push_up函數

3:如果是區間更新,還需要思考:如何設置lazytag?如何用設置的lazy_tag更新所需數據值?

     Lazytag的設置,主要是要求:求sum的時候,根據從父區間傳下來的lazy_tag,能正確更新子區間所有內部數據值和他的lazy值,從而正確求出sum

 

 

 

線段樹的存儲結構:

假設有某個結點,它的信息存儲在 arr [x] 中,那么它的左子節點信息存儲在 arr [2x] 中,右結點信息存儲在 arr [2x+1]中

並且,arr [1] 為整個樹的根節點 ,所以,整體的統計信息、最終的答案,是存在節點1中的。

 

 

 

線段樹的代碼組成:

1:build函數:用於建立整個線段樹。

2:upgrade,修改(更新)函數。注意區分:單點更新、區間更新。

3:push_up: 父節點回溯更新函數

如果是區間更新,還需要:push_down,用於:把lazy_tag往下推一級,推到該節點的子節點,並更新該節點左右子節點的:數據值、lazy_tag值。

 

 

一、build函數:構建線段樹

原理:

開始時是區間[1,n] ,通過遞歸來逐步分解;

線段樹對於每個n的分解是唯一的,所以n相同的線段樹,結構相同。

 

先向下遞歸地尋找葉節點所在位置(尋找長度為1的區間),

一旦找到就把值放到這個位置,

並向上回溯地修改所有父節點的值。

 

 

遞歸遍歷的順序,十分類似於樹的后續遍歷。

例子:N == 10 ;原數組a:1,2,3,4,5,6,7,8,9,10;要求:區間和

 

過程如圖:

 

那么問題來了,究竟要遞歸到多深,才到可以存放原始數據的葉節點??

這取決於:[l,r]什么時候有l==r;其實質是,取決於總結點的個數N.

這也就是為什么我們說 “線段樹對於每個n的分解是唯一的,n相同的線段樹,結構相同。”

 

代碼:

void build(int l,int r,int root)//線段樹建樹
{
    if(l == r)
    {
        num[root]=1;
        return;
    }

    // [l,mid]:左子樹  [mid+1,r]:右子樹
    int mid = (l+r)/2;
build(l,mid,root*2);
build(mid+1,r,root*2+1);

push_up(root);
//對於這個根節點root,先把左右子樹都算出來了,再來更新它的值。 //沿路回溯。回溯到的點root,都是被 [la , rb] 或其子區間影響到的點,邊回溯邊更新 } //調用:build(1,N,1);

 

 

 

 

 二、線段樹的點修改

首先由根節點1向下遞歸,找到對應的葉節點,

然后,修改葉節點的值,

再向上返回,在函數返回的過程中,更新路徑上的節點的統計信息。

 

 

void upgrade(int p,int val,int l,int r,int root)
//單點更新的upgrade算法:把找點p,當成找區間[p,p]
{
    if(l==r)
    {
        num[root]+=val;
        return ;
    }

    int mid = (l+r)/2;
    if(p>mid)    //如果p>當前區間的中點,說明我想找的[p,p]區間,在右半邊
        upgrade(p,val,mid+1,r,root*2+1);
    else
        upgrade(p,val,l,mid,root*2);
    
    
    //沿路回溯。回溯到的點root,都是被[p,p]區間影響到的點,邊回溯邊更新
    num[root] = num[root*2] + num[root*2+1];
}

 

 

三、線段樹的區間修改 —— 引入 lazy_tag

 

和點修改一樣,也是將區間分成子區間,

首先由根節點1向下遞歸,找到對應的葉節點,

然后,修改本節點的值,

再向上返回,在函數返回的過程中,更新路徑上的節點的統計信息。

 

由此可見向上更新的部分,是一毛一樣的。

 

不同的是,點修改不用向下修改(因為對下面的沒有影響,並且也沒有下一級節點了,它自己就是最底層葉節點),

可是區間修改,理論上來說是需要向下修改的(因為父區間變動,子區間也會變動)

 

但是又不能,每次一區間修改,就連帶着修改下面的所有子結點,這也tm太慢了。

 

所以,我們引入了 lazy_tag , 這樣就可以不用把區間內的每個點都按照單點更新的方式更新一遍。

我們暫時不更新下面的子節點,而是打上一個標記,什么時候要用到子節點了,什么時候再看着這個標簽,更新子節點。

(一般都是sum的時候用push_down更新子節點,因為求和的時候需要先知道兩個子節點的值)

 

lazy_tag的含義:

本節點的統計信息已經根據標記更新過了,但是本節點的子節點仍需要進行更新。(注意:打上懶惰標記的節點,它自己是已經更新過了的)

 

即,如果要給一個區間的所有值都加上1,那么,實際上並沒有給這個區間的所有值都加上1,而是打個標記,記下來,這個節點所包含的區間需要加1.

打上標記后,要根據標記更新本節點的統計信息,比如,如果本節點維護的是區間和,而本節點包含5個數,那么,打上+1的標記之后,要給本節點維護的和+5。

 

這是向下延遲修改,但是向上顯示的信息是修改以后的信息,所以查詢的時候可以得到正確的結果。

 

有的標記之間會相互影響,所以比較簡單的做法是,每遞歸到一個區間,首先下推標記(若本節點有標記,就下推標記),然后再打上新的標記,這樣仍然每個區間操作的復雜度是O(log2(n))。

 

重申,請注意:區間修改才需要懶惰標記,單點修改不需要。

 

示例代碼:

void upgrade(int la,int rb,int l,int r ,int val,int root)
{
    /*以后就永遠設定:
       la、rb為需更新區間的左、右端點(一直不變);
       l、r為當前區間的左、右端點,(隨遞歸更新)
       root為當前 [l , r ] 對應的根存儲位置(隨遞歸更新)
    */

//若本次所看區間,整個就包含在所要查詢的區間之內
    if(la<=l && rb>=r)
    {
        num[root] = (r-l+1)*val; //把本區間num更新為正確值
        lazy[root] = val; //增加lazy標記,表示:本區間的Sum正確,子區間的Sum仍需要根據lazy的值來調整
        return ;
    }
    
    push_down(root,r-l+1);
//在繼續遞歸之前,先把當前root 的標記往下推
//每次都是先下推,再更新,從而保證,計算root的時候,它的左右兩子樹都已經是正確值,左右兩子樹都不存在lazy更新延遲,都已經更新好了。

    int mid = (l+r)/2;
    if(la<=mid)
    {
        upgrade(la,rb,l,mid,val,root*2);
    }
    if(rb>mid)
    {
        upgrade(la,rb,mid+1,r,val,root*2+1);
    }


    push_up(root);

}

 

四、push_up函數

我的目標是,已知兩個子節點的某些信息,利用push_up函數推出父節點

思考:如果要用兩個子節點推出父節點,我需要知道子節點的那些信息?————這些信息都是線段樹需要維護的。

然后,已知了所有所需信息之后,具體該怎么推?————決定了:如何具體的寫出 push_up函數

注意:push_up函數內,兩個子節點的信息,是已經更新過的、正確的信息。因為在調用push_up之前,已經調用過push_down函數,也就是說,兩個子節點的信息,是已經被更新過了的,是正確的。

 

五、push_down函數

一、push_down函數總共需要干三件事:

1:更新左右子節點的lazy值,也就是:把父節點root上面的lazy標記,下推到兩個子節點上

2:依據lazy_tag的定義,更新左右子節點的num值,使左右子節點的值成為“被更新過的、正確的值”。

3:把父節點root自身的lazy清空。因為lazy_tag已經被下推,就向上查詢來看,父節點自身的lazy_tag已經不需要了

二、注意:

1:開始一切動作之前,要先特判:此父節點上究竟有沒有lazy_tag。如果本就沒有lazy_tag,千萬別更新,會wa。

2:lazy_tag的意義可以被任意定義;關於‘一’中的“更新”,具體的更新方法也是多種多樣的,但關鍵是:

     lazy_tag的定義,要能跟 “更新num的操作”對應得上。

     也就是說,必須要能夠根據lazy_tag,正確更新結點的num值才可以。

 

示例代碼:

(此處lazy_tag存儲的是:當前段中,每個結點被更新成的數值)

(num的意義是:此區間上的數值累加和)

(所以根據這里lazy_tag的定義,每個num 的更新方法就是:lazy的數值*區間長度)

void push_down(int root,int len)  //傳入:父節點root 和 區間長度len
{ 
    if(lazy[root] == 0)  //如果此節點根本沒有lazy_tag,直接返回,不作處理
        return;

    lazy[root*2] = lazy[root];       //把lazy下推到兩個子節點上
    lazy[root*2+1] = lazy[root];

    num[root*2] = lazy[root*2]*(len-(len)/2);  //更新兩個子節點的num
    num[root*2+1] = lazy[root*2+1]*((len)/2);、
    
    lazy[root]=0;  //清空父節點自身的lazy_tag
}

 

 

整體的示例代碼:

//線段樹——————區間修改,區間查詢
//題意:一個線性數組,不斷把某區間內的值修改“為”另一個輸入值,查詢區間內和。

#include <iostream>
#include <cstdio>
#include <cstring>
#define maxn 100005
int num[maxn*4];//開四倍空間
int lazy[maxn*4];
using namespace std;

void push_up(int root)//根節點狀態更新
{
    num[root] = num[root*2] + num[root*2+1];
}
void build(int l,int r,int root)//線段樹建樹
{
    if(l == r)
    {
        num[root]=1;
        return;
    }
    int mid = (l+r)/2;
    build(l,mid,root*2);
    build(mid+1,r,root*2+1);
    push_up(root);
}

void push_down(int root,int len)//傳入:root結點下標、對應的當前[l,r]區間長度
{
    if(lazy[root] == 0)//假若這個節點根本沒有lazy_tag
        return;
    lazy[root*2] = lazy[root];
    lazy[root*2+1] = lazy[root];
    num[root*2] = lazy[root*2]*(len-(len)/2);
    num[root*2+1] = lazy[root*2+1]*((len)/2);

    
    lazy[root]=0;
}

void upgrade(int la,int rb,int l,int r ,int val,int root)//線段更新
//la、rb為需更新的區間左、右端點,l、r為當前區間左、右端點,root為當前l、r對應的根存儲位置
{
    if(la<=l && rb>=r)
    {
        num[root] = (r-l+1)*val;
        lazy[root] = val;
        return ;
    }
    
    push_down(root,r-l+1);
    
    int mid = (l+r)/2;
    if(la<=mid)
    {
        upgrade(la,rb,l,mid,val,root*2);
    }
    if(rb>mid)
    {
        upgrade(la,rb,mid+1,r,val,root*2+1);
    }
    push_up(root);
}


int query(int la,int rb,int l,int r,int root)//查詢區間[la,rb]的值
{
    if(l>=la && r<=rb)
    {
        return num[root];
    }
    
    push_down(root,r-l+1);
    
    int mid = (l+r)/2;
    int ans=0;
    if(la<=mid)
    {
        ans += query(la,rb,l,mid,root*2);
    }
    if(rb>mid)
    {
        ans +=query(la,rb,mid+1,r,root*2+1);
    }
    
    return ans;
}
int main()
{
    int t;
    scanf("%d",&t);
    for(int o=1;o<=t;o++)
    {
        int N;//原數組結點總數
        scanf("%d",&N);
        memset(num,0,sizeof(num));
        memset(lazy,0,sizeof(lazy));
        
        build(1,N,1);//建樹
        int x;
        scanf("%d",&x);
        while(x--)
        {
            int la,rb,val;
            scanf("%d%d%d",&la,&rb,&val);
            
            upgrade(la,rb,1,N,val,1);//更新[l,r]區間結點,每個結點都被更新“成”val
            query(la,rb,1,N,1);//查詢區間[la,rb]的值
        }
    }
    return 0;
}
View Code

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM