1. 程式人生 > 其它 >初見 | 資料結構 | 線段樹

初見 | 資料結構 | 線段樹

前言

因為用來裝 Win To Go 的硬碟炸了,所以今天下午就水一個部落格罷。

下面是目錄:

目錄

引入

首先我們需要知道線段樹是用來解決什麼問題的資料結構。

先看她的名字來進行大膽的猜測:

線段樹,顧名思義,是和線段有關的樹,那麼其實線段樹就是一種維護區間資訊的資料結構。

基本結構分析

和樹狀陣列的基本結構類似,線段樹也是把一個序列分為各種大小的區間,再按照相應的包含關係構成一棵樹。

說的不是很清楚?上圖看看。

我們可以清晰的看到最小的節點是長度為 1 的區間,她的父節點都是包含她的更大的區間。顯然的是對於長度為 \(n\)

的區間,節點總個數為 \(2n\)

在傳統遞迴建樹下,我們建樹的複雜度是 \(O(n\log n)\) 的。

這裡是堆式儲存,我們要開 \(4n\) 的陣列,至於緣由詳見這一篇文章:線段樹為什麼要開4倍空間 - 拾月悽辰 - 部落格園 (cnblogs.com)

如果您不想去看,我在這裡大約簡述一下:因為我們建樹的時候建出來的不一定是一棵滿二叉樹,甚至都不是一顆完全二叉樹,因此 \(2n\) 的陣列空間肯定不夠用。實際上我們最大節點的編號是在 \(3n\)\(4n\) 之間的,因此我們開 \(4n\) 的資料才能保證不會發生越界。

同時,還有一種建樹的方式叫做動態開點。理論上動態開點的時間複雜度也是 \(O(n\log n)\)

的,只是動態開點能夠節省陣列空間至 \(2n\)

下面我來結合程式碼講解如何實現線段樹的基本結構。

實現

我先會對每一個操作進行實現,然後再給出全部程式。

這裡使用的莉題是 浴谷 P3373 【模板】線段樹 2

預設源

因為我的預設源很長,所以為了節約篇幅,就先把預設源放在這裡。

#define Heriko return
#define Deltana 0
#define S signed
#define LL long long
#define R register
#define I inline
#define lc(x) (x<<1)
#define rc(x) (x<<1|1)
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
using namespace std;
template<typename T>
I void fr(T &x) {...//快讀}
template<typename T>
I void fw(T x,bool k) {...//快輸}

建樹

要致富,先建樹。

線段樹,先建樹。

為了方便,這裡我是用的結構體來記錄節點,\(\texttt{Pushup}\) 函式是合併資訊。

struct node
{
    LL val,add,mul;
}
t[MXX];
I void pushup(LL rt)
{
    t[rt].val=(t[rc(rt)].val+t[lc(rt)].val)%mod;
}

由於我本篇的剩餘程式碼基本都是按照傳統的遞迴建樹寫的,因此這裡的建樹的方法就是傳統遞迴建樹。

傳統遞迴建樹 Code

void build(LL rt,LL l,LL r)
{
    t[rt].add=0;t[rt].mul=1;//區間加tag 和 區間乘tag
    if(l==r) {fr(t[rt].val);Heriko;}
    LL mid=(l+r)>>1;
    build(lc(rt),l,mid);
    build(rc(rt),mid+1,r);
    pushup(rt);
}

至於動態開點,可以自行 BDFS。這裡提供一個我覺得講的還可以的:演算法學習筆記(49): 線段樹的拓展 - 知乎 (zhihu.com)

簡單解釋一下上面的程式碼。先是把 tag 清零,然後進行二分繼續建樹,最後上傳區間值。

下傳標記

考慮到直接對區間內的每一個數進行加法是非常慢的,所以我們用到一個叫做 \(\texttt{Lazy Tag}\) 的東西來優化時間複雜度。當我們要查詢 / 進行操作的時候再把 Tag 下傳,這樣在每次查詢 / 修改之前,每個節點的值都已經及時的更新完畢。

因為這道題是有區間加法和區間乘法兩個操作,所以我們要維護兩個 Tag。

要注意的是要先處理乘法 Tag 再處理加法 Tag。

標記下傳 Code

I void pushdown(LL rt,LL l)
{
    t[lc(rt)].val=(t[lc(rt)].val*t[rt].mul+t[rt].add*(l-(l>>1)))%mod;
    t[rc(rt)].val=(t[rc(rt)].val*t[rt].mul+t[rt].add*(l>>1))%mod;
    (t[lc(rt)].mul*=t[rt].mul)%=mod;
    (t[rc(rt)].mul*=t[rt].mul)%=mod;
    t[lc(rt)].add=(t[lc(rt)].add*t[rt].mul+t[rt].add)%mod;
    t[rc(rt)].add=(t[rc(rt)].add*t[rt].mul+t[rt].add)%mod;
    t[rt].add=0;t[rt].mul=1;
}

區間修改

區間乘

實際上就是打上標記然後二分遞迴完事~

Code

void mul(LL rt,LL l,LL r,LL x,LL y,LL val)
{
    if(x<=l and r<=y)
    {
        (t[rt].val*=val)%=mod;
        (t[rt].mul*=val)%=mod;
        (t[rt].add*=val)%=mod;
        Heriko;
    }
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    if(x<=mid) mul(lc(rt),l,mid,x,y,val);
    if(y>mid) mul(rc(rt),mid+1,r,x,y,val);
    pushup(rt);
}

要注意在繼續遞迴前先 \(\texttt{Pushdown}\),以及在遞迴完成後 \(\texttt{Pushup}\),否則你會像我一樣調億年

區間加

實際上區間加的操作和區間乘基本上是一致的。

Code

void add(LL rt,LL l,LL r,LL x,LL y,LL val)
{
    if(x<=l and r<=y)
    {
        (t[rt].add+=val)%=mod;
        (t[rt].val+=val*(r-l+1))%=mod;
        Heriko;
    }
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    if(x<=mid) add(lc(rt),l,mid,x,y,val);
    if(y>mid) add(rc(rt),mid+1,r,x,y,val);
    pushup(rt);
}

查詢

因為是查詢值所以我們一樣去遞迴的累加子樹值即可。

LL query(LL rt,LL l,LL r,LL x,LL y)
{
    if(x<=l and r<=y) Heriko t[rt].val%mod;
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    LL ans=0;
    if(x<=mid) ans+=query(lc(rt),l,mid,x,y)%mod;
    if(y>mid) ans+=query(rc(rt),mid+1,r,x,y)%mod;
    Heriko ans%mod;
}

全部程式碼

最後就是全部程式碼,實際上如果整體來看,線段樹唯一要記住的就是 \(\texttt{Pushdown}\) 的程式碼,其餘的就都是很自然的遞迴修改,遞迴統計。

...//預設源放在上面了
CI MXX=1e6+5;
LL n,m,mod;
struct node
{
    LL val,add,mul;
}
t[MXX];
I void pushup(LL rt)
{
    t[rt].val=(t[rc(rt)].val+t[lc(rt)].val)%mod;
}
void build(LL rt,LL l,LL r)
{
    t[rt].add=0;t[rt].mul=1;
    if(l==r) {fr(t[rt].val);Heriko;}
    LL mid=(l+r)>>1;
    build(lc(rt),l,mid);
    build(rc(rt),mid+1,r);
    pushup(rt);
}
I void pushdown(LL rt,LL l)
{
    t[lc(rt)].val=(t[lc(rt)].val*t[rt].mul+t[rt].add*(l-(l>>1)))%mod;
    t[rc(rt)].val=(t[rc(rt)].val*t[rt].mul+t[rt].add*(l>>1))%mod;
    (t[lc(rt)].mul*=t[rt].mul)%=mod;
    (t[rc(rt)].mul*=t[rt].mul)%=mod;
    t[lc(rt)].add=(t[lc(rt)].add*t[rt].mul+t[rt].add)%mod;
    t[rc(rt)].add=(t[rc(rt)].add*t[rt].mul+t[rt].add)%mod;
    t[rt].add=0;t[rt].mul=1;
}
void mul(LL rt,LL l,LL r,LL x,LL y,LL val)
{
    if(x<=l and r<=y)
    {
        (t[rt].val*=val)%=mod;
        (t[rt].mul*=val)%=mod;
        (t[rt].add*=val)%=mod;
        Heriko;
    }
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    if(x<=mid) mul(lc(rt),l,mid,x,y,val);
    if(y>mid) mul(rc(rt),mid+1,r,x,y,val);
    pushup(rt);
}
void add(LL rt,LL l,LL r,LL x,LL y,LL val)
{
    if(x<=l and r<=y)
    {
        (t[rt].add+=val)%=mod;
        (t[rt].val+=val*(r-l+1))%=mod;
        Heriko;
    }
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    if(x<=mid) add(lc(rt),l,mid,x,y,val);
    if(y>mid) add(rc(rt),mid+1,r,x,y,val);
    pushup(rt);
}
LL query(LL rt,LL l,LL r,LL x,LL y)
{
    if(x<=l and r<=y) Heriko t[rt].val%mod;
    pushdown(rt,r-l+1);
    LL mid=(l+r)>>1;
    LL ans=0;
    if(x<=mid) ans+=query(lc(rt),l,mid,x,y)%mod;
    if(y>mid) ans+=query(rc(rt),mid+1,r,x,y)%mod;
    Heriko ans%mod;
}
LL x,l,r,val;
S main()
{
    fr(n),fr(m),fr(mod);
    build(1,1,n);
    while(m--)
    {
        fr(x);
        if(x==1) {fr(l),fr(r),fr(val);mul(1,1,n,l,r,val);}
        else if(x==2) {fr(l),fr(r),fr(val);add(1,1,n,l,r,val);}
        else {fr(l),fr(r);fw(query(1,1,n,l,r)%mod);}
    }
    Heriko Deltana;
}

End

\[\texttt{Segment Tree!}\\ \texttt{Too Mach Water!} \]

在這篇完稿時,開頭所說的裝 Win To Go 的硬碟能用了,也算是首尾呼應罷(確信)。

參考 / 引用資料