1. 程式人生 > >線段樹(1)

線段樹(1)

name 復雜度 子樹查詢 cheng 麻煩 通過 front .com i+1

目錄

一、概述

二、從一個例子理解線段樹

  創建線段樹

  線段樹區間查詢

  單節點更新

  區間更新

三、線段樹實戰

--------------------------

一 概述

線段樹,類似區間樹,它在各個節點保存一條線段(數組中的一段子數組),主要用於高效解決連續區間的動態查詢問題,由於二叉結構的特性,它基本能保持每個操作的復雜度為O(logn)。

線段樹的每個節點表示一個區間,子節點則分別表示父節點的左右半區間,例如父親的區間是[a,b],那麽(c=(a+b)/2)左兒子的區間是[a,c],右兒子的區間是[c+1,b]。

二 從一個例子理解線段樹

下面我們從一個經典的例子來了解線段樹,問題描述如下:從數組arr[0...n-1]中查找某個數組某個區間內的最小值,其中數組大小固定,但是數組中的元素的值可以隨時更新。

對這個問題一個簡單的解法是:遍歷數組區間找到最小值,時間復雜度是O(n),額外的空間復雜度O(1)。當數據量特別大,而查詢操作很頻繁的時候,耗時可能會不滿足需求。

另一種解法:使用一個二維數組來保存提前計算好的區間[i,j]內的最小值,那麽預處理時間為O(n^2),查詢耗時O(1), 但是需要額外的O(n^2)空間,當數據量很大時,這個空間消耗是龐大的,而且當改變了數組中的某一個值時,更新二維數組中的最小值也很麻煩。

我們可以用線段樹來解決這個問題:預處理耗時O(n),查詢、更新操作O(logn),需要額外的空間O(n)。根據這個問題我們構造如下的二叉樹

  • 葉子節點是原始組數arr中的元素
  • 非葉子節點代表它的所有子孫葉子節點所在區間的最小值

例如對於數組[2, 5, 1, 4, 9, 3]可以構造如下的二叉樹(背景為白色表示葉子節點,非葉子節點的值是其對應數組區間內的最小值,例如根節點表示數組區間arr[0...5]內的最小值是1): 本文地址

技術分享圖片

由於線段樹的父節點區間是平均分割到左右子樹,因此線段樹是完全二叉樹,對於包含n個葉子節點的完全二叉樹,它一定有n-1個非葉節點,總共2n-1個節點

,因此存儲線段是需要的空間復雜度是O(n)。那麽線段樹的操作:創建線段樹、查詢、節點更新 是如何運作的呢(以下所有代碼都是針對求區間最小值問題)?

2.1 創建線段樹

對於線段樹我們可以選擇和普通二叉樹一樣的鏈式結構。由於線段樹是完全二叉樹,我們也可以用數組來存儲,下面的討論及代碼都是數組來存儲線段樹,節點結構如下(註意到用數組存儲時,有效空間為2n-1,實際空間確不止這麽多,比如上面的線段樹中葉子節點1、3雖然沒有左右子樹,但是的確占用了數組空間,實際空間是滿二叉樹的節點數目: 技術分享圖片技術分享圖片 是樹的高度,但是這個空間復雜度也是O(n)的 )。

struct SegTreeNode

{

  int val;

};

定義包含n個節點的線段樹 SegTreeNode segTree[n],segTree[0]表示根節點。那麽對於節點segTree[i],它的左孩子是segTree[2*i+1],右孩子是segTree[2*i+2]。

我們可以從根節點開始,平分區間,遞歸的創建線段樹,線段樹的創建函數如下:

技術分享圖片
 1 const int MAXNUM = 1000;
 2 struct SegTreeNode
 3 {
 4     int val;
 5 }segTree[MAXNUM];//定義線段樹
 6 
 7 /*
 8 功能:構建線段樹
 9 root:當前線段樹的根節點下標
10 arr: 用來構造線段樹的數組
11 istart:數組的起始位置
12 iend:數組的結束位置
13 */
14 void build(int root, int arr[], int istart, int iend)
15 {
16     if(istart == iend)//葉子節點
17         segTree[root].val = arr[istart];
18     else
19     {
20         int mid = (istart + iend) / 2;
21         build(root*2+1, arr, istart, mid);//遞歸構造左子樹
22         build(root*2+2, arr, mid+1, iend);//遞歸構造右子樹
23         //根據左右子樹根節點的值,更新當前根節點的值
24         segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
25     }
26 }
技術分享圖片

2.2 查詢線段樹

已經構建好了線段樹,那麽怎樣在它上面超找某個區間的最小值呢?查詢的思想是選出一些區間,使他們相連後恰好涵蓋整個查詢區間,因此線段樹適合解決“相鄰的區間的信息可以被合並成兩個區間的並區間的信息”的問題。代碼如下,具體見代碼解釋

技術分享圖片
 1 /*
 2 功能:線段樹的區間查詢
 3 root:當前線段樹的根節點下標
 4 [nstart, nend]: 當前節點所表示的區間
 5 [qstart, qend]: 此次查詢的區間
 6 */
 7 int query(int root, int nstart, int nend, int qstart, int qend)
 8 {
 9     //查詢區間和當前節點區間沒有交集
10     if(qstart > nend || qend < nstart)
11         return INFINITE;
12     //當前節點區間包含在查詢區間內
13     if(qstart <= nstart && qend >= nend)
14         return segTree[root].val;
15     //分別從左右子樹查詢,返回兩者查詢結果的較小值
16     int mid = (nstart + nend) / 2;
17     return min(query(root*2+1, nstart, mid, qstart, qend),
18                query(root*2+2, mid + 1, nend, qstart, qend));
19 
20 }
技術分享圖片

舉例說明(對照上面的二叉樹):

1、當我們要查詢區間[0,2]的最小值時,從根節點開始,要分別查詢左右子樹,查詢左子樹時節點區間[0,2]包含在查詢區間[0,2]內,返回當前節點的值1,查詢右子樹時,節點區間[3,5]和查詢區間[0,2]沒有交集,返回正無窮INFINITE,查詢結果取兩子樹查詢結果的較小值1,因此結果是1.

2、查詢區間[0,3]時,從根節點開始,查詢左子樹的節點區間[0,2]包含在區間[0,3]內,返回當前節點的值1;查詢右子樹時,繼續遞歸查詢右子樹的左右子樹,查詢到非葉節點4時,又要繼續遞歸查詢:葉子節點4的節點區間[3,3]包含在查詢區間[0,3]內,返回4,葉子節點9的節點區間[4,4]和[0,3]沒有交集,返回INFINITE,因此非葉節點4返回的是min(4, INFINITE) = 4,葉子節點3的節點區間[5,5]和[0,3]沒有交集,返回INFINITE,因此非葉節點3返回min(4, INFINITE) = 4, 因此根節點返回 min(1,4) = 1。

2.3單節點更新

單節點更新是指只更新線段樹的某個葉子節點的值,但是更新葉子節點會對其父節點的值產生影響,因此更新子節點後,要回溯更新其父節點的值。

技術分享圖片
 1 /*
 2 功能:更新線段樹中某個葉子節點的值
 3 root:當前線段樹的根節點下標
 4 [nstart, nend]: 當前節點所表示的區間
 5 index: 待更新節點在原始數組arr中的下標
 6 addVal: 更新的值(原來的值加上addVal)
 7 */
 8 void updateOne(int root, int nstart, int nend, int index, int addVal)
 9 {
10     if(nstart == nend)
11     {
12         if(index == nstart)//找到了相應的節點,更新之
13             segTree[root].val += addVal;
14         return;
15     }
16     int mid = (nstart + nend) / 2;
17     if(index <= mid)//在左子樹中更新
18         updateOne(root*2+1, nstart, mid, index, addVal);
19     else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子樹中更新
20     //根據左右子樹的值回溯更新當前節點的值
21     segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
22 }
技術分享圖片

比如我們要更新葉子節點4(addVal = 6),更新後值變為10,那麽其父節點的值從4變為9,非葉結點3的值更新後不變,根節點更新後也不變。

2.4 區間更新

區間更新是指更新某個區間內的葉子節點的值,因為涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麽回溯需要更新的非葉子節點也會有很多,如果一次性更新完,操作的時間復雜度肯定不是O(lgn),例如當我們要更新區間[0,3]內的葉子節點時,需要更新出了葉子節點3,9外的所有其他節點。為此引入了線段樹中的延遲標記概念,這也是線段樹的精華所在。

延遲標記:每個節點新增加一個標記,記錄這個節點是否進行了某種修改(這種修改操作會影響其子節點),對於任意區間的修改,我們先按照區間查詢的方式將其劃分成線段樹中的節點,然後修改這些節點的信息,並給這些節點標記上代表這種修改操作的標記。在修改和查詢的時候,如果我們到了一個節點p,並且決定考慮其子節點,那麽我們就要看節點p是否被標記,如果有,就要按照標記修改其子節點的信息,並且給子節點都標上相同的標記,同時消掉節點p的標記。

因此需要在線段樹結構中加入延遲標記域,本文例子中我們加入標記與addMark,表示節點的子孫節點在原來的值的基礎上加上addMark的值,同時還需要修改創建函數build 和 查詢函數 query,修改的代碼用紅色字體表示,其中區間更新的函數為update,代碼如下:

技術分享圖片
  1 const int INFINITE = INT_MAX;
  2 const int MAXNUM = 1000;
  3 struct SegTreeNode
  4 {
  5     int val;
  6     int addMark;//延遲標記
  7 }segTree[MAXNUM];//定義線段樹
  8 
  9 /*
 10 功能:構建線段樹
 11 root:當前線段樹的根節點下標
 12 arr: 用來構造線段樹的數組
 13 istart:數組的起始位置
 14 iend:數組的結束位置
 15 */
 16 void build(int root, int arr[], int istart, int iend)
 17 {
 18     segTree[root].addMark = 0;//----設置標延遲記域
 19     if(istart == iend)//葉子節點
 20         segTree[root].val = arr[istart];
 21     else
 22     {
 23         int mid = (istart + iend) / 2;
 24         build(root*2+1, arr, istart, mid);//遞歸構造左子樹
 25         build(root*2+2, arr, mid+1, iend);//遞歸構造右子樹
 26         //根據左右子樹根節點的值,更新當前根節點的值
 27         segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
 28     }
 29 }
 30 
 31 /*
 32 功能:當前節點的標誌域向孩子節點傳遞
 33 root: 當前線段樹的根節點下標
 34 */
 35 void pushDown(int root)
 36 {
 37     if(segTree[root].addMark != 0)
 38     {
 39         //設置左右孩子節點的標誌域,因為孩子節點可能被多次延遲標記又沒有向下傳遞
 40         //所以是 “+=”
 41         segTree[root*2+1].addMark += segTree[root].addMark;
 42         segTree[root*2+2].addMark += segTree[root].addMark;
 43         //根據標誌域設置孩子節點的值。因為我們是求區間最小值,因此當區間內每個元
 44         //素加上一個值時,區間的最小值也加上這個值
 45         segTree[root*2+1].val += segTree[root].addMark;
 46         segTree[root*2+2].val += segTree[root].addMark;
 47         //傳遞後,當前節點標記域清空
 48         segTree[root].addMark = 0;
 49     }
 50 }
 51 
 52 /*
 53 功能:線段樹的區間查詢
 54 root:當前線段樹的根節點下標
 55 [nstart, nend]: 當前節點所表示的區間
 56 [qstart, qend]: 此次查詢的區間
 57 */
 58 int query(int root, int nstart, int nend, int qstart, int qend)
 59 {
 60     //查詢區間和當前節點區間沒有交集
 61     if(qstart > nend || qend < nstart)
 62         return INFINITE;
 63     //當前節點區間包含在查詢區間內
 64     if(qstart <= nstart && qend >= nend)
 65         return segTree[root].val;
 66     //分別從左右子樹查詢,返回兩者查詢結果的較小值
 67     pushDown(root); //----延遲標誌域向下傳遞
 68     int mid = (nstart + nend) / 2;
 69     return min(query(root*2+1, nstart, mid, qstart, qend),
 70                query(root*2+2, mid + 1, nend, qstart, qend));
 71 
 72 }
 73 
 74 /*
 75 功能:更新線段樹中某個區間內葉子節點的值
 76 root:當前線段樹的根節點下標
 77 [nstart, nend]: 當前節點所表示的區間
 78 [ustart, uend]: 待更新的區間
 79 addVal: 更新的值(原來的值加上addVal)
 80 */
 81 void update(int root, int nstart, int nend, int ustart, int uend, int addVal)
 82 {
 83     //更新區間和當前節點區間沒有交集
 84     if(ustart > nend || uend < nstart)
 85         return ;
 86     //當前節點區間包含在更新區間內
 87     if(ustart <= nstart && uend >= nend)
 88     {
 89         segTree[root].addMark += addVal;
 90         segTree[root].val += addVal;
 91         return ;
 92     }
 93     pushDown(root); //延遲標記向下傳遞
 94     //更新左右孩子節點
 95     int mid = (nstart + nend) / 2;
 96     update(root*2+1, nstart, mid, ustart, uend, addVal);
 97     update(root*2+2, mid+1, nend, ustart, uend, addVal);
 98     //根據左右子樹的值回溯更新當前節點的值
 99     segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
100 }
技術分享圖片

區間更新舉例說明:當我們要對區間[0,2]的葉子節點增加2,利用區間查詢的方法從根節點開始找到了非葉子節點[0-2],把它的值設置為1+2 = 3,並且把它的延遲標記設置為2,更新完畢;當我們要查詢區間[0,1]內的最小值時,查找到區間[0,2]時,發現它的標記不為0,並且還要向下搜索,因此要把標記向下傳遞,把節點[0-1]的值設置為2+2 = 4,標記設置為2,節點[2-2]的值設置為1+2 = 3,標記設置為2(其實葉子節點的標誌是不起作用的,這裏是為了操作的一致性),然後返回查詢結果:[0-1]節點的值4;當我們再次更新區間[0,1](增加3)時,查詢到節點[0-1],發現它的標記值為2,因此把它的標記值設置為2+3 = 5,節點的值設置為4+3 = 7;

其實當區間更新的區間左右值相等時([i,i]),就相當於單節點更新,單節點更新只是區間更新的特例。

三 線段樹實戰

求區間的最大值、區間求和等問題都是采用類似上面的延遲標記域。下面會通過acm的一些題目來運用一下線段樹。

等待更新......

參考資料

GeeksforGeeks: http://www.geeksforgeeks.org/segment-tree-set-1-range-minimum-query/

GeeksforGeeks: http://www.geeksforgeeks.org/segment-tree-set-1-sum-of-given-range/

懂得博客[數據結構之線段樹]:http://dongxicheng.org/structure/segment-tree/

MetaSeed[數據結構專題—線段樹]: http://blog.csdn.net/metalseed/article/details/8039326

NotOnlySuccess[完全版 線段樹]: http://www.notonlysuccess.com/index.php/segment-tree-complete/

【版權聲明】轉載請註明出處:http://www.cnblogs.com/TenosDoIt/p/3453089.html

線段樹(1)