線段樹的一點總結

留念處_燈火闌珊發表於2016-07-28
線段樹,顧名思義,是根據線段建成的樹。每一個節點都可以是線段。對於單點查詢,區間查詢,單點更新,區間更新都是O(logn)級別的,所以對於大多數區間操作比較大的題,都可以用線段樹解決。
0.性質
對於每一個非葉子節點下標為i的節點,它的左兒子的下標必定為I<<1,右兒子的下標必定為I<<1|1.

1.定義

const int MAXN = 100005;//線段最大長度
struct Node{
    int l;//區間的左端點
    int r;//區間的右端點
    int v;//區間儲存的資料
}tree[MAXN<<2];//一般是開4倍,但是聽大牛說最多隻有3點幾倍

2.建樹

void Buildtree(int i,int left,int right)//當前節點的下標及左右端點的座標
{
    tree[i].l=left;//給左右端點賦值
    tree[i].r=right;

    if(left==right)
    {
        scanf("%d",&tree[i].v);//給葉子節點賦值
        return ;
    }

    int mid=(left+right)>>1;
    Buildtree(i<<1,left,mid);//折半向左右遞迴建樹
    Buildtree(i<<1|1,mid+1,right);
}

3.區間查詢

int Query(int i,int left,int right)//當前區間的下標和需要查詢的左右端點
{
    if(tree[i].l>=left&&tree[i].r<=right)//噹噹前區間已經被覆蓋的時候,直接return節點資訊
        return tree[i].v;

    int mid=(tree[i].l+tree[i].r)>>1;
    if(left<=mid)Query(i<<1,left,right);//分別向左右子節點查詢
    if(mid<right)Query(i<<1|1,left,right);
}

4.單點更新

void Modi(int i,int pos,int v)//當前區間的下標,需要修改的位置以及需要被修改成的值
{
    if(tree[i].l==pos&&tree[i].r==pos)//找到該位置,修改然後返回
    {
        tree[i].v=v;
        return ;
    }

    int mid=(tree[i].l+tree[i].r)>>1;
    if(pos<=mid)Modi(i<<1,pos,v);//分別向左右子節點查詢
    if(mid<pos)Modi(i<<1|1,pos,v);
}

小結:以上就是線段樹最基礎的實現,可以做到單點更新,區間查詢。初學線段樹,重點就是要體會遞迴的思想。

推薦題目:HDU 1166,HDU 2795,HDU 1394,HDU 1754



5.區間更新
這個是初學者的一道坎,學會了這個,才算踏入了線段樹的大門。我們需要用到一個標記,又叫LazyTag,看名字就知道,這是一個很懶的標記,那麼這個標記是用來幹什麼的呢?我們稍後再講。首先我們來討論一下區間更新的時間複雜度。很容易想到,如果我們把區間看做一個一個的點,那麼只需要一個迴圈遍歷所有的點然後對於每一個點單點更新即可,那麼時間複雜度是多少呢?對於長度為length的區間更新,這樣做的時間複雜度為O(length*logn)如果對於多次長度很大的查詢,這樣做顯然很笨重,那麼這個時候我們就需要用到LazyTag了。我們首先在我們最開始定義的結構體中加上一個成員Tag,然後在函式Buildtree裡面加上它的初始化,一般的,我們初始化為0,但是對於具體情況還是要具體分析,不能一味的套模板。

void PushDown(int st)
{
    if(tree[st].tag!=0)
    {
        tree[st<<1].v+=tree[st].v;//向左右子節點傳遞標記
        tree[st<<1].tag+=tree[st].tag;
        tree[st<<1|1].v+=tree[st].v;
        tree[st<<1|1].tag+=tree[st].tag;
        tree[st].tag=0;//傳遞過後,標記被清零
    }
}

void Update(int i,int left,int right,int v)
{
    if(tree[i].l>=left&&tree[i].r<=right)
    {
        tree[i].v+=v;//一般做題的時候當前節點的值是要被更新後才打上標記
        tree[i].tag+=v;//打上標記
        return ;
    }

    PushDown(i);
    int mid=(tree[i].l+tree[i].r)>>1;
    if(left<=mid)Update(i<<1,left,right,v);//左右向左右子節點遞迴
    if(mid<right)Update(i<<1|1,left,right,v);
}

推薦題目:HDU 1556,HDU 1698,POJ 3468

6.區間合併
從這個地方開始就有點困難了,一般的線段樹只能對給定的區間進行操作,但是如果我們需要查詢滿足某種需要的最大區間呢?那麼我們就需要對具有相同性質的區間進行合併。我們一般是向我們定義的結構體中新增三個成員,lsum,rsum,msum。這三個成員分別表示當前區間從左,右端點開始滿足條件的最大長度和整個區間滿足條件的最大長度。
對於子節點,我們可以很容易的判斷出它是否滿足條件,那麼我們就從子節點向上更新即可,例如:

void PushUp(int i,int len)//更新的節點下標和更新的區間長度
{
    tree[i].lsum=tree[i<<1].lsum;//當前節點的lsum是由左子節點貢獻而來
    if(tree[i].lsum==(len-(len>>1)))tree[i].lsum+=tree[i<<1|1].lsum;//如果lsum整個區間的長度相同,那麼我們需要加上右子節點的lsum
    tree[i].rsum=tree[i<<1|1].rsum;
    if(tree[i].rsum==(len>>1))tree[i].rsum+=tree[i<<1].rsum;
    tree[i].msum=max(tree[i<<1].rsum+tree[i<<1|1].lsum,max(tree[i<<1].msum,tree[i<<1|1].msum))//整個區間的msum就是左右子區間的msum和左子區間的rsum加上右區間的lsum中的最大值
}

當然,區間合併也不是直接套用模板就行的,我們需要對具體問題進行分析,才能確定如何進行區間合併。

推薦題目:HDU 3308,POJ 3667
7.掃描線
這個一般是和離散化聯絡起來的,想象出一個線,沿著一個方向掃過去。一般都是與離散化的方向垂直。思考清楚然後注意一下資料突變的點就沒什麼難的了。
(這個東西沒有什麼固定的套路(可能是因為我太弱不知道),還是需要多寫,熟練了就好)

推薦題目:HDU 1542 ,HDU 1828


總結:線段樹雖然不算高階資料結構,但是很好用,能夠解決很多與區間有關的問題,因為它優秀的時間複雜度,熟練掌握它能加深您對遞迴和二叉樹儲存的理解。當然,以上這些並不是線段樹的全部,如果您熟練掌握了以上線段樹的技巧之後,還意猶未盡的話,您可以去看看《【zkw線段樹講稿】統計的力量-線段樹》,在那個裡面,我感覺線段樹的性質被淋漓盡致地展現出來,十分精彩。

相關文章