線段樹(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個節點
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)