淺談線段樹

                                                                         數據結構——線段樹html

O、引例node

A.給出n個數,n<=100,和m個詢問,每次詢問區間[l,r]的和,並輸出。數據結構

一種回答:這也太簡單了,O(n)枚舉搜索就好了。ide

另外一種回答:還用得着o(n)枚舉,前綴和o(1)就搞定。優化

那好,我再修改一下題目。ui

B.給出n個數,n<=100,和m個操做,每一個操做可能有兩種:一、在某個位置加上一個數;二、詢問區間[l,r]的和,並輸出。spa

回答:o(n)枚舉。code

動態修改最起碼不能用靜態的前綴和作了。htm

好,我再修改題目:blog

C.給出n個數,n<=1000000,和m個操做,每一個操做可能有兩種:一、在某個位置加上一個數;二、詢問區間[l,r]的和,並輸出。

回答:o(n)枚舉絕對超時。

再改:

D,給出n個數,n<=1000000,和m個操做,每一個操做修改一段連續區間[a,b]的值

回答:從a枚舉到b,一個一個改。。。。。。有點兒常識的人都知道超時

那怎麼辦?這就須要一種強大的數據結構:線段樹

1、基本概念

一、線段樹是一棵二叉搜索樹,它儲存的是一個區間的信息。

二、每一個節點以結構體的方式存儲,結構體包含如下幾個信息:

     區間左端點、右端點;(這二者必有)

     這個區間要維護的信息(事實際狀況而定,數目不等)。

三、線段樹的基本思想:二分

四、線段樹通常結構如圖所示:

五、特殊性質:

由上圖可得,

一、每一個節點的左孩子區間範圍爲[l,mid],右孩子爲[mid+1,r]

二、對於結點k,左孩子結點爲2*k,右孩子爲2*k+1,這符合徹底二叉樹的性質

2、線段樹的基礎操做

注:如下基礎操做均以引例中的求和爲例,結構體以此爲例:

struct node
{
       int l,r,w;//l,r分別表示區間左右端點,w表示區間和
}tree[4*n+1];

線段樹的基礎操做主要有5個:

建樹、單點查詢、單點修改、區間查詢、區間修改。

一、建樹,即創建一棵線段樹

   ① 主體思路:a、對於二分到的每個結點,給它的左右端點肯定範圍。

                     b、若是是葉子節點,存儲要維護的信息。

                     c、狀態合併。

  ②代碼

void build(int l,int r,int k)
{
    tree[k].l=l;tree[k].r=r;
    if(l==r)//葉子節點 
    {
        scanf("%d",&tree[k].w);
        return ; 
    }
    int m=(l+r)/2;
    build(l,m,k*2);//左孩子 
    build(m+1,r,k*2+1);//右孩子 
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//狀態合併,此結點的w=兩個孩子的w之和 
}

③注意

 a.結構體要開4倍空間,爲啥本身畫一個[1,10]的線段樹就懂了

 b.千萬不要漏了return語句,由於到了葉子節點不須要再繼續遞歸了。

二、單點查詢,即查詢一個點的狀態,設待查詢點爲x

   ①主體思路:與二分查詢法基本一致,若是當前枚舉的點左右端點相等,即葉子節點,就是目標節點。若是不是,由於這是二分法,因此設查詢位置爲x,當前結點區間範圍爲了l,r,中點爲         mid,則若是x<=mid,則遞歸它的左孩子,不然遞歸它的右孩子

   ②代碼

void ask(int k)
{
    if(tree[k].l==tree[k].r) //當前結點的左右端點相等,是葉子節點,是最終答案 
    {
        ans=tree[k].w;
        return ;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask(k*2);//目標位置比中點靠左,就遞歸左孩子 
    else ask(k*2+1);//反之,遞歸右孩子 
}

  ③正確性分析:

     由於若是不是目標位置,由if—else語句對目標位置定位,逐步縮小目標範圍,最後必定能只到達目標葉子節點。

三、單點修改,即更改某一個點的狀態。用引例中的例子,對第x個數加上y

①主體思路

 結合單點查詢的原理,找到x的位置;根據建樹狀態合併的原理,修改每一個結點的狀態。

 ②代碼

void add(int k)
{
    if(tree[k].l==tree[k].r)//找到目標位置 
    {
        tree[k].w+=y;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) add(k*2);
    else add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//全部包含結點k的結點狀態更新 
}

四、區間查詢,即查詢一段區間的狀態,在引例中爲查詢區間[x,y]的和

①主體思路

 

 

mid=(l+r)/2

y<=mid ,即 查詢區間全在,當前區間的左子區間,往左孩子走

x>mid 即 查詢區間全在,當前區間的右子區間,往右孩子走

不然,兩個子區間都走

②代碼

void sum(int k)
{
    if(tree[k].l>=x&&tree[k].r<=y) 
    {
        ans+=tree[k].w;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) sum(k*2);
    if(y>m) sum(k*2+1);
}

③正確性分析

狀況1,3不用說,對於狀況2,最差狀況是搜到葉子節點,此時必定知足狀況1

五、區間修改,即修改一段連續區間的值,咱們已給區間[a,b]的每一個數都加x爲例講解

    Ⅰ.引子

 

       有人可能就想到了:

       修改的時候只修改對查詢有用的點。

       對,這就是區間修改的關鍵思路。

      爲了實現這個,咱們引入一個新的狀態——懶標記

  Ⅱ 懶標記

     (懶標記比較難理解,我盡力講明白。。。。。。)

       一、直觀理解:「懶」標記,懶嘛!用到它才動,不用它就睡覺。

       二、做用:存儲到這個節點的修改信息,暫時不把修改信息傳到子節點。就像家長扣零花錢,你用的時候纔給你,不用不給你。

       三、實現思路(重點):

           a.原結構體中增長新的變量,存儲這個懶標記。

           b.遞歸到這個節點時,只更新這個節點的狀態,並把當前的更改值累積到標記中。注意是累積,能夠這樣理解:過年,不少個親戚都給你壓歲錢,但你暫時不用,因此都被你父母扣下了。

           c.何時纔用到這個懶標記?當須要遞歸這個節點的子節點時,標記下傳給子節點。這裏沒必要管用哪一個子節點,兩個都傳下去。就像你若是還有妹妹,父母給大家零花錢時總不能偏愛吧

           d.下傳操做:

               3部分:①當前節點的懶標記累積到子節點的懶標記中。

                         ②修改子節點狀態。在引例中,就是原狀態+子節點區間點的個數*父節點傳下來的懶標記

                            這就有疑問了,既然父節點都把標記傳下來了,爲何還要乘父節點的懶標記,乘本身的不行嗎?

                            由於本身的標記多是父節點屢次傳下來的累積,每次都乘本身的懶標記形成重複累積

                         ③父節點懶標記清0。這個懶標記已經傳下去了,不清0後面再用這個懶標記時會重複下傳。就像你父母給了你5元錢,你不能說由於前幾回給了你10元錢, 因此此次給了你15元,那你不就虧大了。 

     懶標記下穿代碼:f爲懶標記,其他變量與前面含義一致。

void down(int k)
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}

 Ⅲ 完整的區間修改代碼:

void add(int k)
{
    if(tree[k].l>=a&&tree[k].r<=b)//當前區間所有對要修改的區間有用 
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1區間點的總數
        tree[k].f+=x;
        return;
    }
    if(tree[k].f) down(k);//懶標記下傳。只有不知足上面的if條件才執行,因此必定會用到當前節點的子節點 
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) add(k*2);
    if(b>m) add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改區間狀態 
}

 Ⅳ.懶標記的引入對其餘基本操做的影響

     由於引入了懶標記,不少用不着的更改狀態存了起來,這就會對區間查詢、單點查詢形成必定的影響。

     因此在使用了懶標記的程序中,單點查詢、區間查詢也要像區間修改那樣,對用獲得的懶標記下傳其實就是加上一句if(tree[k].f)  down(k),其他不變

     2017.5.16 以前寫的單點修改不須要下傳懶標記,在此訂正:單點修改也須要下傳懶標記

     引入了懶標記的單點查詢代碼:

 void ask(int k)//單點查詢
{
    if(tree[k].l==tree[k].r)
    {
        ans=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);//懶標記下傳,惟一須要更改的地方
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask(k*2);
    else ask(k*2+1);
}

    引入了懶標記的區間查詢代碼:

void sum(int k)//區間查詢
{
    if(tree[k].l>=x&&tree[k].r<=y) 
    {
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f)  down(k)//懶標記下傳,惟一須要更改的地方
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) sum(k*2);
    if(y>m) sum(k*2+1);
}

3、總結

線段樹5種基本操做代碼:

#include<cstdio>
using namespace std;
int n,p,a,b,m,x,y,ans;
struct node
{
    int l,r,w,f;
}tree[400001];
inline void build(int k,int ll,int rr)//建樹 
{
    tree[k].l=ll,tree[k].r=rr;
    if(tree[k].l==tree[k].r)
    {
        scanf("%d",&tree[k].w);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)//標記下傳 
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}
inline void ask_point(int k)//單點查詢
{
    if(tree[k].l==tree[k].r)
    {
        ans=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask_point(k*2);
    else ask_point(k*2+1);
}
inline void change_point(int k)//單點修改 
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].w+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) change_point(k*2);
    else change_point(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w; 
}
inline void ask_interval(int k)//區間查詢 
{
    if(tree[k].l>=a&&tree[k].r<=b) 
    {
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}
inline void change_interval(int k)//區間修改 
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*y;
        tree[k].f+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) change_interval(k*2);
    if(b>m) change_interval(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main()
{
    scanf("%d",&n);//n個節點 
    build(1,1,n);//建樹 
    scanf("%d",&m);//m種操做 
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&p);
        ans=0;
        if(p==1)
        {
            scanf("%d",&x);
            ask_point(1);//單點查詢,輸出第x個數 
            printf("%d",ans);
        } 
        else if(p==2)
        {
            scanf("%d%d",&x,&y);
            change_point(1);//單點修改 
        }
        else if(p==3)
        {
            scanf("%d%d",&a,&b);//區間查詢 
            ask_interval(1);
            printf("%d\n",ans);
        }
        else
        {
             scanf("%d%d%d",&a,&b,&y);//區間修改 
             change_interval(1);
        }
    }
}

 

 4、空間優化

父節點k,左二子2*k,右兒子2*k+1,須要4*n的空間

但並非全部的葉子節點佔用到2n+1——4n

這就形成大量空間浪費

2*n空間表示法:推薦博客:http://www.cppblog.com/MatoNo1/archive/2015/05/05/195857.html

用dfs序表示作節點下標

父節點k,左兒子k+1,右兒子:k+左兒子區間長度*2,不是父節點下標+父節點區間長度。由於當樹不滿時,二者不相等

具體實現這裏就再也不寫模板了,就是改改左右兒子的下標

可參考代碼: 題目:樓房重建 http://www.cnblogs.com/TheRoadToTheGold/p/6361242.html 

 

裏面的建樹用的2*n空間

5、模板題

一、codevs 1080 線段樹練習 (單點修改+區間查詢)  http://codevs.cn/problem/1080/  

#include<cstdio>
using namespace std;
int n,m,p,x,y,ans;
struct node
{
    int l,r,w;
}tree[400001];
inline void build(int l,int r,int k)
{
    tree[k].l=l;tree[k].r=r;
    if(l==r) 
    {
        scanf("%d",&tree[k].w);
        return ;
    }
    int m=(l+r)/2;
    build(l,m,k*2);
    build(m+1,r,k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void add(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].w+=y;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) add(k*2);
    else add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w; 
}
inline void sum(int k)
{
    if(tree[k].l>=x&&tree[k].r<=y) 
    {
        ans+=tree[k].w;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) sum(k*2);
    if(y>m) sum(k*2+1);
}
int main()
{
    scanf("%d",&n);
    build(1,n,1);
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&p,&x,&y);
        ans=0;
        if(p==1) add(1);
        else 
        {
            sum(1);
            printf("%d\n",ans);
        }
    }
}
View Code

二、codevs 1081 線段樹練習2 (單點查詢+區間修改) http://codevs.cn/problem/1081/

#include<cstdio>
using namespace std;
int n,p,a,b,m,x,ans;
struct node
{
    int l,r,w,f;
}tree[400001];
inline void build(int k,int ll,int rr)
{
    tree[k].l=ll,tree[k].r=rr;
    if(tree[k].l==tree[k].r)
    {
        scanf("%d",&tree[k].w);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}
inline void add(int k)
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*x;
        tree[k].f+=x;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) add(k*2);
    if(b>m) add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void ask(int k)
{
    if(tree[k].l==tree[k].r)
    {
        ans=tree[k].w;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask(k*2);
    else ask(k*2+1); 
}
int main()
{
    scanf("%d",&n);
    build(1,1,n);
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&p);
        if(p==1)
        {
            scanf("%d%d%d",&a,&b,&x);
            add(1);
        }
        else
        {
            scanf("%d",&x);
            ask(1);
            printf("%d\n",ans);
        }
    }
}
View Code

三、codevs 1082 線段樹練習3  (區間修改+區間查詢)

#include<cstdio>
using namespace std;
int n,p,a,b,m,x,y;
long long ans;
struct node
{
    long long l,r,w,f;
}tree[800001];
inline void build(int k,int ll,int rr)//建樹 
{
    tree[k].l=ll,tree[k].r=rr;
    if(tree[k].l==tree[k].r)
    {
        scanf("%d",&tree[k].w);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)//標記下穿 
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}
inline void ask_interval(int k)//區間查詢 
{
    if(tree[k].l>=a&&tree[k].r<=b) 
    {
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}
inline void change_interval(int k)//區間修改 
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*y;
        tree[k].f+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) change_interval(k*2);
    if(b>m) change_interval(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main()
{
    scanf("%d",&n); 
    build(1,1,n);
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&p);
        ans=0;
        if(p==1) 
        {
             scanf("%d%d%d",&a,&b,&y);//區間修改 
             change_interval(1);
        }
        else 
        {
            scanf("%d%d",&a,&b);//區間查詢 
            ask_interval(1);
            printf("%lld\n",ans);
        }
    
    }
}
View Code

6、經典例題

> codevs 3981/SPOJ GSS1/GSS3 ——區間最大子段和
> Bzoj3813 奇數國——區間內某個值是否出現過
>洛谷 P2894 酒店 Hotel ——區間連續一段空的長度
> codevs 2421 /Bzoj1858 序列操做——多種操做
> codevs 2000 / BZOJ 2957: 樓房重建——區間的最長上升子序列
 Codevs3044 矩形面積求並——掃描線

代碼的話到隨筆分類——線段樹裏找找吧 http://www.cnblogs.com/TheRoadToTheGold/category/933602.html

相關文章
相關標籤/搜索