線段樹是一種二叉搜索樹,與區間樹類似,它將一個區間劃分紅一些單元區間,每一個單元區間對應線段樹中的一個葉結點。使用線段樹能夠快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲 $O(\log N)$ 。而未優化的空間複雜度爲 $2N$ ,所以有時須要離散化讓空間壓縮。——by 百度數組
首先明確一件事,根($root$)的左孩子是$root \cdot 2 $或$root << 1$,右孩子是$root \cdot 2+1$或$root<< 1 | 1$函數
咱們有個大小爲 $5$ 的數組 $a={10,11,12,13,14}$ 要進行區間求和操做,如今咱們要怎麼把這個數組存到線段樹中(也能夠說是轉化成線段樹)呢?咱們這樣子作:設線段樹的根節點編號爲 $1$ ,用數組 $d$ 來保存咱們的線段樹, $d[i]$ 用來保存編號爲 $i$ 的節點的值(這裏節點的值就是這個節點所表示的區間總和),如圖所示:
上圖來自$oi-wiki$優化
void build(int root,int l,int r) { if(l==r) {//若是到了葉子節點就直接賦值 tree[root]=a[l]; return; } int mid=(l+r)/2; build(root*2,l,mid);//遞歸左子樹 build(root*2+1,mid+1,r);//遞歸右子樹 tree[root]=tree[root*2]+tree[root*2+1];//注意須要更新根節點 }
咱們來看一下$build$函數的運行過程,當從$root$開始遞歸時遞歸了左子樹,左子樹又遞歸左子樹,一直到葉節點返回了左葉節點的值,而後和上面的同樣去遞歸右子樹,一直到葉而後返回了右葉節點的值(上面描述的可能不是太清楚,能夠本身結合一下上圖圖),而後一層一層的返回就能夠了ui
區間查詢,好比求區間 $[l,r]$ 的總和(即 $a[l]+a[l+1]+ \cdots +a[r]$ )、求區間最大值/最小值……還有不少不少……怎麼作呢?
3d
如上圖舉例
若是要查詢區間 $[1,5]$ 的和,那直接獲取 $d[1]$ 的值( $60$ )便可。那若是我就不查詢區間 $[1,5]$ ,我就查區間 $[3,5]$ 呢?code
傻了吧。但其實呢咱們確定仍是有辦法的!htm
你要查的不是 $[3,5]$ 嗎?我把 $[3,5]$ 拆成 $[3,3]$ 和 $[4,5]$ 不就好了嗎?blog
int query(int root,int l,int r,int x,int y) { if(x<=l && r<=y) return tree[root]; int mid=(l+r)/2; int ans=0; pushdown(root,l,r,mid); if(x<=mid) ans+=query(root*2,l,mid,x,y); if(mid<y) ans+=query(root*2+1,mid+1,r,x,y); return ans; }
這裏就是線段樹的精髓了,請仔細理解遞歸
區間修改是個頗有趣的東西……你想啊,若是你要修改區間 $[l,r]$ ,難道把全部包含在區間[l,r]中的節點都遍歷一次、修改一次?那估計這時間複雜度估計會上天。這怎麼辦呢?咱們這裏要引用一個叫作 「懶惰標記」 的東西。
咱們設一個數組 $b$ , $b[i]$ 表示編號爲 $i$ 的節點的懶惰標記值。啥是懶惰標記、懶惰標記值呢?這裏我再舉個例子:
A 有兩個兒子,一個是 B,一個是 C。
有一天 A 要建一個新房子,沒錢。恰好過年嘛,有人要給 B 和 C 紅包,兩個紅包的錢數相同都是 $(1000000000000001\bmod 2)$ 圓(好多啊!……不就是 $1$ 元嗎……),然而由於 A 是父親因此紅包確定是先塞給 A 咯~
理論上來說 A 應該把兩個紅包分別給 B 和 C,可是……缺錢嘛,A 就把紅包偷偷收到本身口袋裏了。
A 高興地說:「我如今有 $2$ 份紅包了!我又多了 $2\times (1000000000000001\bmod 2)=2$ 元了!哈哈哈~」
可是 A 知道,若是他不把紅包給 B 和 C,那 B 和 C 確定會不爽而後致使家庭矛盾最後崩潰,因此 A 對兒子 B 和 C 說:「我欠大家每人 $1$ 份 $(1000000000000001\bmod 2)$ 圓的紅包,下次有新紅包給過來的時候再給大家!這裏我先作下記錄……嗯……我錢大家各 $(1000000000000001\bmod 2)$ 圓……」
兒子 B、C 有點惱怒:「但是若是有同窗問起咱們咱們收到了多少紅包咋辦?你把咱們的紅包都收了,咱們還怎麼裝X?」
父親 A 趕緊說:「有同窗問起來我就會給大家的!我欠條都寫好了不會不算話的!」
這樣 B、C 才放了心。
在這個故事中咱們不難看出,A 就是父親節點,B 和 C 是 A 的兒子節點,並且 B 和 C 是葉子節點,分別對應一個數組中的值(就是以前講的數組 $a$ ),咱們假設節點 A 表示區間 $[1,2]$ (即 $a[1]+a[2]$ ),節點 B 表示區間 $[1,1]$ (即 $a[1]$ ),節點 C 表示區間 $[2,2]$ (即 $a[2]$ ),它們的初始值都爲 $0$ (如今纔剛開始呢,還沒拿到紅包,因此都沒錢~)。
如圖:
注:這裏 D 表示當前節點的值(即所表示區間的區間和)。
爲何節點 A 的 D 是 $2\times (1000000000000001\bmod 2)$ 呢?緣由很簡單:節點 A 表示的區間是 $[1,2]$ ,一共包含 $2$ 個元素。咱們是讓 $[1,2]$ 這個區間的每一個元素都加上 $1000000000000001\bmod 2$ ,因此節點 A 的值就加上了 $2\times (1000000000000001\bmod 2)$ 咯。
若是這時候咱們要查詢區間 $[1,1]$ (即節點 B 的值)怎麼辦呢?不是說了嗎?若是 B 要用到的時候,A 就把它欠的還給 B!
具體是這樣操做(如圖):
注:爲何是加上 $1\times (1000000000000001\bmod 2)$ 呢?
緣由和上面同樣——B 和 C 表示的區間中只有 $1$ 個元素啊!
由此咱們能夠獲得,區間 $[1,1]$ 的區間和就是 $1$ 啦!O(∩_∩)O 哈哈~!
PS:上述解釋來自$Oi-wiki$,我以爲解釋的很好能夠看看,附上上面解釋的原版代碼
void update(int l, int r, int c, int s, int t,int p){ // [l,r] 爲修改區間,c 爲被修改的元素的變化量,[s,t] 爲當前節點包含的區間,p 爲當前節點的編號 if (l <= s && t <= r) { d[p] += (t - s + 1) * c, b[p] += c; return; }// 當前區間爲修改區間的子集時直接修改當前節點的值,而後打標記,結束脩改 int m = (s + t) / 2; if (b[p] && s!=t){ // 若是當前節點的懶標記非空,則更新當前節點兩個子節點的值和懶標記值 d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m); b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 將標記下傳給子節點 b[p] = 0; // 清空當前節點的標記 } if (l <= m) update(l, r, c, s, m, p * 2); if (r > m) update(l, r, c, m + 1, t, p * 2 + 1); d[p] = d[p * 2] + d[p * 2 + 1]; }
下面是個人代碼:
void update(int root,int l,int r,int x,int y,int v) { if(x<=l && r<=y) return add(root,l,r,v); int mid=(l+r)/2; pushdown(root,l,r,mid); if(x<=mid) update(root*2,l,mid,x,y,v); if(y>mid) update(root*2+1,mid+1,r,x,y,v); tree[root]=tree[root*2]+tree[root*2+1]; }
void add(int root,int l,int r,int v) { tree[root]+=v*(r-l+1); lazy[root]+=v; }
void pushdown(int root,int l,int r,int mid) { if(lazy[root]==0) return ; add(root*2,l,mid,lazy[root]); add(root*2+1,mid+1,r,lazy[root]); lazy[root]=0; }
單節點更新是指只更新線段樹的某個葉子節點的值,可是更新葉子節點會對其父節點的值產生影響,所以更新子節點後,要回溯更新其父節點的值。
/* 功能:更新線段樹中某個葉子節點的值 root:當前線段樹的根節點下標 [nstart, nend]: 當前節點所表示的區間 index: 待更新節點在原始數組arr中的下標 addVal: 更新的值(原來的值加上addVal) */ void updateOne(int root, int nstart, int nend, int index, int addVal) { if(nstart == nend) { if(index == nstart)//找到了相應的節點,更新之 segTree[root].val += addVal; return; } int mid = (nstart + nend) / 2; if(index <= mid)//在左子樹中更新 updateOne(root*2+1, nstart, mid, index, addVal); else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子樹中更新 //根據左右子樹的值回溯更新當前節點的值 segTree[root].val = segTree[root*2+1].val+segTree[root*2+2].val; }