初見 | 資料結構 | 線段樹
前言
因為用來裝 Win To Go 的硬碟炸了,所以今天下午就水一個部落格罷。
下面是目錄:
目錄引入
首先我們需要知道線段樹是用來解決什麼問題的資料結構。
先看她的名字來進行大膽的猜測:
線段樹,顧名思義,是和線段有關的樹,那麼其實線段樹就是一種維護區間資訊的資料結構。
基本結構分析
和樹狀陣列的基本結構類似,線段樹也是把一個序列分為各種大小的區間,再按照相應的包含關係構成一棵樹。
說的不是很清楚?上圖看看。
我們可以清晰的看到最小的節點是長度為 1 的區間,她的父節點都是包含她的更大的區間。顯然的是對於長度為 \(n\)
在傳統遞迴建樹下,我們建樹的複雜度是 \(O(n\log n)\) 的。
這裡是堆式儲存,我們要開 \(4n\) 的陣列,至於緣由詳見這一篇文章:線段樹為什麼要開4倍空間 - 拾月悽辰 - 部落格園 (cnblogs.com)。
如果您不想去看,我在這裡大約簡述一下:因為我們建樹的時候建出來的不一定是一棵滿二叉樹,甚至都不是一顆完全二叉樹,因此 \(2n\) 的陣列空間肯定不夠用。實際上我們最大節點的編號是在 \(3n\) 和 \(4n\) 之間的,因此我們開 \(4n\) 的資料才能保證不會發生越界。
同時,還有一種建樹的方式叫做動態開點。理論上動態開點的時間複雜度也是 \(O(n\log n)\)
下面我來結合程式碼講解如何實現線段樹的基本結構。
實現
我先會對每一個操作進行實現,然後再給出全部程式。
這裡使用的莉題是 浴谷 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 的硬碟能用了,也算是首尾呼應罷(確信)。
參考 / 引用資料
- [1] 線段樹 —— OI Wiki
- [2] [資料結構入門]線段樹 —— Dfkuaid
- [3] 線段樹為什麼要開4倍空間 —— 拾月悽辰
- [4] 演算法學習筆記(49): 線段樹的拓展 - 知乎 (zhihu.com) —— Pecco