數據結構之線段樹

一、引例

有M個數排成一列,做N次操作,每次操作包括:
(1)詢問指定區間的最大值、最小值
(2)將指定區間的每個數加上一個值
如果按照最樸素的做法,一個個的遍歷,時間複雜度:O(MN)。
那麼如何解決一個區間求和(最大值,最小值)的問題呢?那麼就要用到線段樹啦。

二、定義
線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。

主要用來解決區間查詢、區間修改,使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,基本保證每次操作的時間複雜度爲O(logN)。

三、實際應用
a.單點更新

1.定義每個結點的信息
線段樹是建立在線段的基礎上,每個結點都代表了一條線段[a,b]。長度爲1的線段稱爲元線段。非元線段都有兩個子結點,左結點代表的線段爲[a,(a + b) / 2],右結點代表的線段爲[((a + b) / 2)+1,b]。

struct node
{
    int left,right,sum;//左端點,右端點,和
} tree[maxn<<2];

2.更新

void maintain(int root)//更新根節點爲左右子結點的和
{
    int lnode=root<<1;
    int rnode=root<<1+1;
    tree[root].sum=tree[lnode].sum+tree[rnode].sum;
}

3.遞歸建樹
遇到葉子節點直接賦值,否則遞歸遍歷左右建樹,最後回溯即可。

void build(int root,int begin,int end)//樹的結點編號 左端點下標 右端點下標
{
    tree[root].left=begin;
    tree[root].right=end;
    if(begin==end)//葉子節點
    {
        scanf("%d",&adj[begin]);
        tree[root].sum=adj[begin];//單元素 直接賦值
        return ;
    }
    int mid=(begin+end)>>1;
    build(root<<1,begin,mid);//更新左子樹
    build(root<<1+1,mid+1,end);//更新右子樹
    maintain(root);//存儲左右子樹的和
}

4.單點更新
將一條線段[a,b] 插入到代表線段[l,r]的結點p中,如果p不是元線段,那麼令mid=(l+r)/2。如果b<mid,那麼將線段[a,b] 也插入到p的左兒子結點中,如果a>mid,那麼將線段[a,b] 也插入到p的右兒子結點中。

void update(int root,int pos,int num)
        //根結點編號 欲修改值的下標 期待的值
{
    if(tree[root].left==tree[root].right&&tree[root].left==pos)
    {//若修改的值在這個節點的左右區間之間那麼就直接更改此區間的sum值就可
        tree[root].sum+=num;
        return ;
    }
    int mid=(tree[root].left+tree[root].right)>>1;
    if(pos<=mid)
        update(root<<1,pos,num);
    else
        update(root<<1+1,pos,num);
    maintain(root);//每次都要更新根節點
}

5.求和操作

int query(int root,int begin,int end)//求和
{
    int ans=0;
    if(begin==tree[root].left&&end==tree[root].right)
    {
        return tree[root].sum;
    }
    int mid=(tree[root].left+tree[root].right)>>1;
    if(end<=mid)
        ans+=query(root<<1,begin,end);
    else if(begin>=mid+1)
        ans+=query(root<<1+1,begin,end);
    else
    {
        ans+=query(root<<1,begin,mid);
        ans+=query(root<<1+1,mid+1,end);
    }
    return ans;
}

b.區間更新(成段更新)

比如 從[1,10]每個結點的值都+1,普通單點更新就會超時。
*區間更新:
指更新某個區間內的葉子節點的值,因爲涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麼回溯需要更新的非葉子節點也會有很多,如果一次性更新完,操作的時間複雜度肯定不是O(lgn),例如當我們要更新區間[0,3]內的葉子節點時,需要更新出了葉子節點3,9外的所有其他節點。爲此引入了線段樹中的延遲標記概念,這也是線段樹的精華所在。

*延遲標記:
因爲更新的數很多,所以我每一步的更新不接着算出來,等到最後需要的時候再去取消標記算出來。

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

用lazy標記,等到當前區間比我需要的目標區間大的時候,我必須用到下面的值了,必須往下修改了,這時候,我們就把之前堆積起來的懶惰標記pushdown了,於是就有了一個神奇的pushdown操作。

其他的建樹什麼的和單點更新一樣,只是多了lazy標記和pushdown。

void pushdown(LL root)  //向下傳遞lazy標記 
{
    if (tree[root].lazy)
    {
        tree[root<<1].lazy+=tree[root].lazy;
        tree[root<<1+1].lazy+=tree[root].lazy;
        tree[root<<1].val+=tree[root<<1].len*tree[root].lazy;
       tree[root<<1+1].val+=tree[root<<1+1].len*tree[root].lazy;
        tree[root].lazy=0; 
    }
}

c.區間合併
不過還沒做過這方面的題QAQ

參考博客
很有趣很好懂
很官方很全面
來做題吧 超全

ps:之前看過,今天再看像重新學了一遍(>_<)
今天距離省賽過去已經一個星期了,該調整回來了。
還是不夠強,繼續修煉吧QAQ。

發佈了87 篇原創文章 · 獲贊 8 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章