《數據結構》線段樹入門(一)


今天介紹一種非常特殊的數據結構——線段樹

首先提出一個問題:


給你n個數,有兩種操作:

1:給第i個數的值增加X


2:詢問區間[a,b]的總和是什么?


輸入描述


輸入文件第一行為一個整數n,接下來是n行n個整數,表示格子中原來的整數。接下一個正整數q,再接
下來有q行,表示q個詢問,第一個整數表示詢問代號,詢問代號1表示增加,后面的兩個數x和A表示給
位置X上的數值增加A,詢問代號2表示區間求和,后面兩個整數表示a和b,表示要求[a,b]之間的區間和。


樣例輸入


4


7 6 3 5


2


1 1 4


2 1 2


樣例輸出


17


數據范圍


1 <= n,q <= 100000


看到這個問題,最朴素的想法是用一個數組模擬,求和時 [ a , b ]中逐個累加 , 最后輸出 。
但是,由於數據量比較大,時間復雜度太高,時間上無法承受。


這時我們可以用線段樹( Segment Tree ),這種特殊的數據結構解決這個問題。


那么什么是線段樹呢?

這就是一棵典型的線段樹

 

 

一 般的線段樹上的每一個節點T[a , b],代表該節點維護了原數列[ a , b ]區間的信息。對於每一個節點他至少有
三個信息:左端點,右端點,我們需要維護的信息(在本題中我們維護區間和)。由於線段樹是一個二叉樹,而且是一個平衡二叉樹,如果當前結點的編號是i,左端點為L ,右端點為 R , 那么左兒子的 編號為 i*2 ,左端點為 L ,右端點為 (L + R)/2 ; 同理右兒子的 編號為 i*2+1,左端點為(L+R)/2 ,右端點為 R
。如果當前結點的左端點等於右端點,那么該節點就是葉子節點,直接在該節點賦值即可。顯然線段樹是遞歸定義的。

 

線段樹就是這樣一種數據結構,講一個大區間分為若干個不相交的區間,每次維護都在小區間上處理,並且查


詢也在這些被分解的區間中信息合並出我們需要的結果,這就是線段樹高效的原因。

 

線段樹的存儲:


線段樹的存儲可用鏈表和數組模擬。(本文采用數組寫法,便於理解)


1.鏈表存儲:

1 struct node
2 {
3     int Left, Right;
4     node *Leftchild , *Rightchild;
5 };

2.數組模擬

1 struct Tree
2 {
3 
4     int l, r;
5     long long sum;
6 
7 } tr[maxN << 2];

注意:數組的空間要開四倍大小,防止訪問越界,(理論上大於maxN的最小2x的兩倍)

 

建樹:

線段樹的構建是自頂點而下,即從根節點開始遞歸構建,根據線段樹定義,當左端點等於右端點時(達到遞歸邊界),直接賦值即可,回溯時也要維護區間,代碼如下:

 1 void Build_Tree ( int x , int y , int i )
 2 {
 3 
 4     tr[i].l = x;
 5     tr[i].r = y;
 6 
 7     if( x == y )tr[i].sum = a[x] ; //找到葉子節點,賦值
 8 
 9     else
10     {
11         ll mid = (tr[i].l tr[i].r ) >> 1 ;
12 
13         Build_Tree ( x , mid , i << 1); //左子樹
14 
15         Build_Tree ( mid + 1 , y , i << 1 | 1); //右子樹
16 
17         tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯維護區間和
18 
19     }
20 }

 

維護樹:


維護樹的方法也很好理解,如果目標更新節點在左兒子里,去左兒子中查找;反之,在右兒子中。不斷遞歸,知道找到需要維護的節點,更新它,回溯是一路更新回來。這就是維護的過程,代碼如下:

 1 void Update_Tree ( int q , int val , int i )
 2 {
 3     if(tr[i].l == q && tr[i].r == q) //找到需要修改的葉子節點
 4     {
 5         tr[i].sum = val ; //更新當前結點
 6     }
 7     else //當前結點是非葉子結點
 8     {
 9         long long mid = (tr[i].l tr[i].r ) >> 1 ; //取中間
10 
11         if ( q <= mid ) //目標節點在左兒子中
12         {
13             Update_Tree ( q , val , i << 1 );
14         }
15         else if( q > mid ) //目標節點在右兒子中
16         {
17             Update_Tree ( q , val , i << 1 | 1 );
18         }
19         tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯
20     }
21 }

 

查詢樹:


題目中讓我們查詢區間求和,不難想到如果當前結點的區間完全被目標區間包含,直接返回當前結點的sum值,

否則分類討論。具體過程通過以下代碼理解:

 1 long long Query_Tree ( int q , int w , int i )
 2 {
 3     if ( q <= tr[i].l && w >= tr[i].r ) return tr[i].sum; //當前結點的區間完全被目標區間包含
 4     else
 5     {
 6         long long mid = (tr[i].l tr[i].r) >> 1;
 7         if( q > mid ) //完全在右兒子
 8         {
 9             return Query_Tree ( q , w , i << 1 | 1);
10         }
11         else if (w <= mid ) //完全在左兒子
12         {
13             return Query_Tree ( q , w , i << 1);
14         }
15         else //目標區間在左右都有分布
16         {
17             return Query_Tree ( q , w , i << 1) + Query_Tree ( q , w , i << 1 | 1 );
18         }
19     }
20 }

 

主程序:

 

 1 int main ( )
 2 {
 3 
 4     int N, M, q, val, l, r;
 5     scanf("%d", &N);
 6     for ( int i = 1 ; i <= N ; i++ )scanf("%d", &a[i]);
 7     Build_Tree ( 1 , N , 1);
 8     cin >> M ;
 9     while (M--)
10     {
11         int op ;
12         cin >> op ;
13         if ( op == 1 )
14         {
15             scanf("%d%d", &q, &val);
16             Update_Tree ( q , val , 1);
17         }
18         else
19         {
20             scanf("%d%d", &l, &r);
21             printf("%lld\n", Query_Tree ( l , r, 1 ));
22         }
23     }
24     return 0 ;
25 }

 

線段樹的性質:


假設線段樹處理的數列長度為N,那么總結點數不超過2*N(滿二叉樹是最大情況);

線段分解數量級:線段樹能把任意一條長度為M的線段分為不超過2Log2(M)條線段(我們知道一個很大的數,Log一下就變小了),這條性質使線段樹的查詢與修改復雜度都在O(Log2(n))的范圍內解決。


由於線段樹是一顆二叉樹,深度約為Log2(N)左右。


綜上,線段樹空間消耗O(n),由於它深度性質,使它在解決問題上有較高的效率。

 



(本期完)

 

 

To Be Continued ;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM