算法學習-線段樹入門詳解-

通過做題目思考:爲什麼我們要學線段樹呢?

這個博客是用結構體寫的線段樹:
下面的拋出兩個問題一起思考一下:

問題1

給1000個正整數,編號1到1000,用a[1],a[2],…,a[1000]表示。
統計:1.編號從L到R的所有數之和爲多少?
其中1<= L <= R <= 1000.

你很快就可以思考出解決方法:

  1. O(n)枚舉搜索
  2. 前綴和O(1)。

問題2:

給出n個數,n<=1000000,和m個操作,每個操作可能有兩種:

  1. 在某個位置加上一個數;
  2. 詢問區間[l,r]的和,並輸出。

O(n)枚舉。(超時呀!!!)那怎麼辦呢?
這個時候也思考一下之前認知裏有的解決問題的方式----前綴和
你也會發現動態修改是不能用靜態的前綴和實現的。

那我們通過思考,可以知道我們所學習的知識對有些問題是無法解決的時候。(學過線段樹或者數組數組的除外)於是,我們就需要學習新的算法了。一種強大的數據結構:線段樹(樹狀數組也是可以做這幾個問題的)

好,接下來,我們開始一步一步走向線段樹:

一.詳細講解線段樹。

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

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

線段樹,是一種二叉搜索樹。它將一段區間劃分爲若干單位區間,每一個節點都儲存着一個區間。雖然說是一個區間,但實際它儲存的是一個區間的信息(數值)。

線段樹基礎思想:分塊?二分?各有看法,但實際用起來就是二分。

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

  2. 對於結點k,左孩子結點爲2k,右孩子爲2k+1,這是完全二叉樹性質。

如下圖:
在這裏插入圖片描述

1.建樹

建樹的實現,思想就是分治,根本上也可以說是二分。

每個節點以結構體的方式存儲(當然以後用數組比較好,節省代碼量減少寫代碼的時間),結構體包含以下幾個信息:

  1. 區間左端點、右端點;
  2. 這個區間要維護的信息(視實際情況而定,數目不等)

實現思路:

利用二分,從二叉樹的根開始。

  1. 二分到每一個節點,給它左右端點確定範圍。

  2. 如果是葉子節點,存儲要維護的信息。(輸入的初始數據。)

  3. 狀態合併。將某個結點w的左右兩個孩子值之和合併到w上。

可以看代碼:

struct node//結構體
{
    int l,r,w;//l,r分別表示區間左右端點,w表示區間和
}tree[15000];//數組開4倍
void build(int l,int r,int k)//k從1 開始
{
    tree[k].l=l,tree[k].r=r;
    if(l==r)//葉子節點
    {
        scanf("%d",&tree[k].w);
        return;
    }
    int mid=(l+r)/2;
    build(l,mid,2*k);//左孩子
    build(mid+1,r,2*k+1);//右孩子
    tree[k].w=tree[2*k].w+tree[2*k+1].w;//狀態合併,此結點的w=兩個孩子的w之和 
}

最後要注意:

  1. 結構體要開4倍空間,看上面畫的一個[1,10]的線段樹就懂了。

  2. 自己寫的時候不要漏了return語句,因爲到了葉子節點不需要再繼續遞歸了。

2.單點查詢

單點,就是一個點進行查詢。因爲線段樹的特殊性,這裏的點,就是左右端點相等的時候的 “區間”,也可以說是葉子節點。

方式:二分查詢:

  1. 如果當前枚舉的點左右端點相等,即葉子節點,就是目標節點。

  2. 如果不是,利用二分思想。對查詢位置爲x,和當前結點區間範圍爲 [l,r],中點爲mid。如果x<=mid,則遞歸它的左孩子,否則遞歸它的右孩子,那麼最後一定可以得出答案。

我們仍然需要證明一下其正確性:
假如不是目標位置,由if—else語句對目標位置定位,逐步縮小目標範圍,最後一定能只到達目標葉子節點。

再詳細一點:
我們二分的情況有三種:

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

3.單點修改

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

int ask(int k)
{
    if(tree[k].l==tree[k].r)//二分到左右端點相等,也就是葉子節點。
    {
        tree[k].w+=y;
    }
    int mid=(tree[k].l+tree[k].r)/2;
    if(x<=mid)
        ask(2*k);
    else
        ask(2*k+1);
    tree[k].w=tree[2*k].w+tree[2*k+1].w;
}

4.區間查詢

mid=(l+r)/2;

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

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

否則,兩個子區間都走

正確性分析

情況1,3不用說,對於情況2,最差情況是搜到葉子節點,此時一定滿足情況1

void sum( int k)
{
    if(tree[k].l<=x&&tree[k].r>=y)//找區間左右端點
    {
        ans+=tree[k].w;//找到目標
        return ;
    }
    int mid=(tree[k].l+tree[k].r)/2;//二分
    if(y<=mid)//查詢區間全在當前區間的左子區間,往左孩子走.
        sum(2*k);
    if(x>mid)//查詢區間全在當前區間的右子區間,往右孩子走.
        sum(2*k+1);
}

5.區間修改

修改的時候只修改對查詢有用的點。
對,這就是區間修改的關鍵思路。
爲了實現這個,我們引入一個新的狀態——懶標記。
Ⅱ 懶標記
(懶標記比較難理解,我盡力講明白。。。。。。)
1、直觀理解:“懶”標記,懶嘛!用到它才動,不用它就睡覺。
2、作用:存儲到這個節點的修改信息,暫時不把修改信息傳到子節點。就像家長扣零花錢,你用的時候纔給你,不用不給你。
3. 實現思路(重點):
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-l)+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),其餘不變。

單點修改也需要下傳懶標記

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);
}
#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);
        }
    }
}

這個博客是用結構體寫的線段樹:
想看用數組寫的可以去看這篇:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章