今天介紹一種非常特殊的數據結構——線段樹
首先提出一個問題:
給你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 ;