1. 程式人生 > >線段樹--從入門到精通

線段樹--從入門到精通

分享圖片 iostream 編號 答案 修改 單點 code 分享 簡單

線段樹,強大的數據結構,用處也是比較廣的。

首先,我們要明白線段樹是個啥?

線段樹,線段嘛,有左右端點,那麽它當然可以代表一個區間,那麽區間上的好多事情都可以用它來搞,比如:區間加,區間乘,區間求和。

首先讓我們先看個線段樹的模型。

技術分享圖片

如圖,這就是一棵線段樹的模型。

圈內的點表示這是第幾個點,紅色表示這個點表示的區間範圍。

每個點和它的左右兩個兒子的編號是有一定的關系的:

點N,它的左兒子編號為N$\times$2,右兒子編號為N$\times$2+1.

線段樹支持單點修改,區間修改,單點查詢,區間查詢。

講解有易到難。

先放一張後邊當例子講解的圖(每個圈中的數表示的為這個區間的和

)。

技術分享圖片

構建線段樹框架

假設一段長度為 N 的序列,那麽我們需要維護總長為 1--N 的線段。

對於每一個點,我們需要確定它所表示的線段的 左端點 右端點 以及我們要維護的區間和

對於每個點的左兒子和右兒子來說,左兒子繼承前一半 [L,(L+R)/2],右兒子繼承後一半( (L+R)/2,R ]。

還有我們維護的區間和,每個大區間都是有兩個小區間組成,那麽 大區間的和 = 左兒子的和+右兒子的和。

這部分代碼:

struct ahah{
    long long l,r,sum,f;  //對於 f 的作用,後邊會有解釋,此處忽略。
}tree[200000<<2];    註意此處四倍空間。
void build(int k,int l,int r) { tree[k].l=l;tree[k].r=r; if(tree[k].l==tree[k].r) { scanf("%lld",&tree[k].sum); return ; } long long mid=(tree[k].l+tree[k].r)>>1; build(k<<1,l,mid); build(k<<1|1,mid+1,r); tree[k].sum=tree[k<<1
].sum+tree[k<<1|1].sum; }

單點查詢與修改

單點修改,我們已知單點的位置,那麽我們從一號點開始,根據兩個兒子所代表的區間範圍,選擇下一步是走左兒子還是右兒子,今兒一步步的確定準確的點。

單點查詢與單點修改幾乎一樣,查詢到具體的位置後,輸出其結果。

拿上邊的圖進行模擬下:

修改4號點:左兒子[0,4],右兒子[5,8] ->選擇左兒子 ->左兒子[0,2],右兒子[3,4] ->選擇右兒子 ->... -> 找到4號點修改。

查詢同上。

當我們修改完某個點以後,包含這個點的區間的和發生了改變,所以最後我們還要加一句:

$tree[k].sum=tree[k \times 2].sum+tree[k \times 2+1].sum$ 以確保維護的區間和不會改變。

代碼:k表示點的編號,需要給x號點加上y 。

void update(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].sum+=y;
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)update(k<<1);
    else update(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

區間求和與修改

區間修改與查詢也有很大的相似。

區間修改,暫時來說我們沒有好辦法,只能一個一個的修改區間中的每一個元素,後邊會有優秀做法的講解。

區間查詢,我們需要明確這個被查詢的區間位置。

以下被查詢的區間用[a,b]表示,k表示當前的點的編號。

首先我們從最大的區間開始,判斷被查詢的區間,有三種情況:

1.位於左兒子中($b \le tree[k<<1].l $)還是右兒子中($a > tree[k<<1|1].l $),然後選擇下一步是去左兒子還是右兒子。

2.被查詢的區間被兩部分都包括,那麽我們就將區間分開,一部分查詢左區間,一部分查詢右區間。

3.現在的點所代表的區間$(a <= tree[k].l , b >= tree[k].r )$ 被要查詢的區間所包含,那麽不需要再查下去,直接將答案加上這段區間所維護的和就好了。

拿查詢區間[3,5]模擬一下:

mid表示當前區間的二分點。

    技術分享圖片

代碼用遞歸實現:

void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);    //先省略就好。
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}

重點來了:懶標記

對於區間修改來說,我們一個一個的修改浪費大量的時間,並且修改了還不一定查修這個點,為了解決這個問題,我們引入懶標記 f 。

首先我們要明確他的一個性質: 懶,用得著的時候動一下,用不著的時候就永遠在那。

每個節點的的懶標記記錄的是它所代表的這個區間所加的值 f 。

就像區間查詢一樣,當區間不被包含時,分開查找,當目前區間已被要修改的區間包含時,那麽我們就可以直接給這個點,打上懶標記,不需要去準確的一個一個的修改區間內的元素了。

那這樣的話必究沒法維護區間和了?

我們維護區間和為的是啥?當然是為了求區間和了,當我們在查詢的時候,若用得到這整個區間,那麽返回 維護的值 + 區間元素個數$\times$懶標記的值,若不全用得到的話,那麽我們將懶標記下傳給它的左右兩個兒子,然後繼續查找。區間和並不是沒有維護,而是在維護懶標記從而間接地維護者區間和。

這裏需要註意的是:當節點的懶標記下傳給兒子的時候它的懶標記則需要清空,因為已經傳給了兒子。

藍標即下傳代碼:

void down(long long k)
{
    tree[k<<1].f+=tree[k].f;
    tree[k<<1|1].f+=tree[k].f;
    tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f;
    tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f;
    tree[k].f=0;
}

用到藍標即的區間加以及求和:

void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}
void add(long long k)
{
    if(tree[k].l>=x&&tree[k].r<=y)
    {
        tree[k].sum+=(tree[k].r-tree[k].l+1)*val;
        tree[k].f+=val;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)add(k<<1);
    if(y>mid)add(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

綜上就是先對簡單的線段樹操作。

貼上模板:

#include<cstdio>
#include<iostream>
using namespace std;

long long n,m,ans,x,y,ch,val;
struct ahah{
    long long l,r,sum,f;
}tree[200000<<2];
void build(int k,int l,int r)
{
    tree[k].l=l;tree[k].r=r;
    if(tree[k].l==tree[k].r)
    {
        scanf("%lld",&tree[k].sum);
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
void update(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].sum+=y;
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)update(k<<1);
    else update(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
void down(long long k)
{
    tree[k<<1].f+=tree[k].f;
    tree[k<<1|1].f+=tree[k].f;
    tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f;
    tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f;
    tree[k].f=0;
}
void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}
void add(long long k)
{
    if(tree[k].l>=x&&tree[k].r<=y)
    {
        tree[k].sum+=(tree[k].r-tree[k].l+1)*val;
        tree[k].f+=val;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)add(k<<1);
    if(y>mid)add(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    build(1,1,n);
    for(int i=1;i<=m;i++)
    {
        ans=0;
        cin>>ch>>x>>y;
        if(ch==1)
        {
            cin>>val;
            add(1);
        }
        else
        {
            query(1);
            cout<<ans<<"\n";
        }
    }
}

例題:

入門

模板:洛谷線段樹1:https://www.luogu.org/problemnew/show/P3372

單點修改與區間查詢:最大數https://www.luogu.org/problemnew/show/P1198

進階:

妖夢斬木棒:https://www.luogu.org/problemnew/show/P3797

無聊的數列:https://www.luogu.org/problemnew/show/P1438

線段樹--從入門到精通