線段樹的入門級總結
線段樹是一種二叉搜尋樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間為[a,(a+b)/2],右兒子表示的區間為[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最後的子節點數目為N,即整個線段區間的長度。
使用線段樹可以快速的查詢某一個節點在若干條線段中出現的次數,時間複雜度為O(logN)。而未優化的空間複雜度為2N,因此有時需要離散化讓空間壓縮。—-來自百度百科
【以下以 求區間最大值為例】
先看宣告:
1 2 3 4 5 6 7 8 9 |
#include #include const int MAXNODE = 2097152; const int MAX = 1000003; struct NODE{ int value; // 結點對應區間的權值 int left,right; // 區間 [left,right] }node[MAXNODE]; int father[MAX]; // 每個點(當區間長度為0時,對應一個點)對應的結構體陣列下標 |
【建立線段樹(初始化)】:
由於線段樹是用二叉樹結構儲存的,而且是近乎完全二叉樹的,所以在這裡我使用了陣列來代替連結串列上圖中區間上面的紅色數字表示了結構體陣列中對應的下標。
在完全二叉樹中假如一個結點的序號(陣列下標)為 I ,那麼 (二叉樹基本關係)
I 的父親為 I/2,
I 的另一個兄弟為 I/2*2 或 I/2*2+1
I 的兩個孩子為 I*2 (左) I*2+1(右)
有了這樣的關係之後,我們便能很方便的寫出建立線段樹的程式碼了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void BuildTree(int i,int left,int right){ // 為區間[left,right]建立一個以i為祖先的線段樹,i為陣列下標,我稱作結點序號 node[i].left = left; // 寫入第i個結點中的 左區間 node[i].right = right; // 寫入第i個結點中的 右區間 node[i].value = 0; // 每個區間初始化為 0 if (left == right){ // 當區間長度為 0 時,結束遞迴 father[left] = i; // 能知道某個點對應的序號,為了更新的時候從下往上一直到頂 return; } // 該結點往 左孩子的方向 繼續建立線段樹,線段的劃分是二分思想,如果寫過二分查詢的話這裡很容易接受 // 這裡將 區間[left,right] 一分為二了 BuildTree(i<<1, left, (int)floor( (right+left) / 2.0)); // 該結點往 右孩子的方向 繼續建立線段樹 BuildTree((i<<1) + 1, (int)floor( (right+left) / 2.0) + 1, right); } |
【單點更新線段樹】:
由於我事先用 father[ ] 陣列儲存過 每單個結點 對應的下標了,因此我只需要知道第幾個點,就能知道這個點在結構體中的位置(即下標)了,這樣的話,根據之前已知的基本關係,就只需要直接一路更新上去即可。
1 2 3 4 5 6 7 8 9 |
void UpdataTree(int ri){ // 從下往上更新(注:這個點本身已經在函式外更新過了) if (ri == 1)return; // 向上已經找到了祖先(整個線段樹的祖先結點 對應的下標為1) int fi = ri / 2; // ri 的父結點 int a = node[fi<<1].value; // 該父結點的兩個孩子結點(左) int b = node[(fi<<1)+1].value; // 右 node[fi].value = (a > b)?(a):(b); // 更新這個父結點(從兩個孩子結點中挑個大的) UpdataTree(ri/2); // 遞迴更新,由父結點往上找 } |
【查詢區間最大值】:
將一段區間按照建立的線段樹從上往下一直拆開,直到存在有完全重合的區間停止。對照圖例建立的樹,假如查詢區間為 [2,5]
紅色的區間為完全重合的區間,因為在這個具體問題中我們只需要比較這 三個區間的值 找出 最大值 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int Max = -1<<20; void Query(int i,int l,int r){ // i為區間的序號(對應的區間是最大範圍的那個區間,也是第一個圖最頂端的區間,一般初始是 1 啦) if (node[i].left == l && node[i].right == r){ // 找到了一個完全重合的區間 Max = (Max < node[i].value)?node[i].value:(Max); return ; } i = i << 1; // get the left child of the tree node if (l <= node[i].right){ // 左區間有涉及 if (r <= node[i].right) // 全包含於左區間,則查詢區間形態不變 Query(i, l, r); else // 半包含於左區間,則查詢區間拆分,左端點不變,右端點變為左孩子的右區間端點 Query(i, l, node[i].right); } i += 1; // right child of the tree if (r >= node[i].left){ // 右區間有涉及 if (l >= node[i].left) // 全包含於右區間,則查詢區間形態不變 Query(i, l, r); else // 半包含於左區間,則查詢區間拆分,與上同理 Query(i, node[i].left, r); } } |