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

線段樹(二)

ref class 搜索 turn 們的 highlight print log max-width

轉自:http://blog.csdn.net/liujian20150808/article/details/51137749

1.線段樹的定義:

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。

對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間為[a,(a+b)/2],右兒子表示的區間為[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最後的子節點數目為N,即整個線段區間的長度。

舉例描述:

因此有了以上對線段樹的定義,我們可以將區間[1,10]的線段樹結構描述出來:

技術分享

圖片來自於百度百科

有了線段[1,10]的線段樹結構,我們可以發現,每個葉節點對應區間範圍內的端點[a,a](1<=a<=10)

2.構造線段樹

顯然,當我們將線段樹定義清楚之後,那麽我們就要想要怎麽去實現它。

我們可以觀察上圖,對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間均為[a,(a+b)/2],右兒子表示的區間均為[(a+b)/2+1,b],

因此我們利用線段樹的這一特點,可以遞歸的將這棵線段樹構造出來,遞歸的終止條件也就是我們構造到了葉節點,即此時線段的左右區間相等。

有了以上的思路,我們可以得出以下構造線段樹的代碼:

[cpp] view plain copy
  1. //由區間[left,right]建立一棵線段樹
  2. Node *Build(Node *_root,int left,int right){
  3. _root = Init(_root,left,right); //節點初始化
  4. if(left != right){ //遞歸終止條件,表示已經線段樹建立到了葉節點
  5. _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);
  6. _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);
  7. }
  8. return _root; //回溯至最後一層時,則返回整棵樹的根節點
  9. }


為了檢驗構造情況是否和上述圖示一致,我們可以利用樹的中序遍歷,查看每個節點存儲的線段,因此我們得出以下完整代碼:

[cpp] view plain copy
  1. #include<cstdio>
  2. #include<cstdlib>
  3. typedef struct node Node;
  4. struct node{
  5. int leftvalue;
  6. int rightvalue; //分別用來記錄節點記錄的區間範圍
  7. Node *lchild; //左孩子節點
  8. Node *rchild; //右孩子節點
  9. };
  10. int flag = 1; //用於標記當前遍歷到哪個節點
  11. //節點的初始化
  12. Node *Init(Node *_node,int lvalue,int rvalue){
  13. _node = (Node *)malloc(sizeof(Node));
  14. _node -> lchild = NULL;
  15. _node -> rchild = NULL;
  16. _node -> leftvalue = lvalue;
  17. _node -> rightvalue = rvalue;
  18. return _node;
  19. }
  20. //由區間[left,right]建立一棵線段樹
  21. Node *Build(Node *_root,int left,int right){
  22. _root = Init(_root,left,right); //節點初始化
  23. if(left != right){ //遞歸終止條件,表示已經線段樹建立到了葉節點
  24. _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);
  25. _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);
  26. }
  27. return _root; //回溯至最後一層時,則返回整棵樹的根節點
  28. }
  29. //中序遍歷,便於從左往右查看各節點存儲的線段區間
  30. void inorder(Node *_node){
  31. if(_node){
  32. inorder(_node -> lchild);
  33. printf("第%d個遍歷的節點,存儲區間為:[%d,%d]\n",flag++,_node -> leftvalue,_node -> rightvalue);
  34. inorder(_node -> rchild);
  35. }
  36. }
  37. int main(){
  38. Node *root;
  39. root = Build(root,1,10);
  40. inorder(root);
  41. return 0;
  42. }

運行結果:
技術分享

我們發現,存儲的結果與一開始定義的完全一致,於是我們便成功的建立好了一棵空的線段樹。

3.線段樹的一些簡單應用:

(1).區間查詢問題:

我們以RMQ為例,即在給定區間內查詢最小值,假設我們已經將對應區間的最小值存入了線段樹的節點中,那麽我們利用剛剛建立好的線段樹來解決這一問題。

如果查詢的區間是[1,2],[3,3]這樣的區間,那麽我們直接找到對應節點解決這一問題即可。但是如果查詢的區間是[1,6],[2,7]這樣的區間時,我們可以發現在線段樹中,無法找到這樣的節點,

但是呢,我們可以找到樹中哪幾個節點能夠組成我們所要求的區間,然後再取這幾個區間內的最小值不就解決問題了嗎?

因此有了這樣的想法,我們對於任何在合理範圍內的查詢,都可以找到若幹個相連的區間,然後將這若幹個區間合並,得到待求的區間。

通常,我們用來尋找這樣的一個區間的簡單辦法是:

function 在節點v查詢區間[l,r]

if v所表示的區間和[l,r]交集不為空集 if v所表示的區間完全屬於[l,r]

選取v

else

在節點v的左右兒子分別查詢區間[l,r]end if end if

end function

偽代碼出自《線段樹》講稿 楊戈

因此根據以上偽代碼我們可以得出以下完整代碼:

[cpp] view plain copy
  1. #include<cstdio>
  2. #include<cstdlib>
  3. typedef struct node Node;
  4. struct node{
  5. int leftvalue;
  6. int rightvalue; //分別用來記錄節點記錄的區間範圍
  7. Node *lchild; //左孩子節點
  8. Node *rchild; //右孩子節點
  9. };
  10. int flag = 1; //用於標記當前遍歷到哪個節點
  11. //節點的初始化
  12. Node *Init(Node *_node,int lvalue,int rvalue){
  13. _node = (Node *)malloc(sizeof(Node));
  14. _node -> lchild = NULL;
  15. _node -> rchild = NULL;
  16. _node -> leftvalue = lvalue;
  17. _node -> rightvalue = rvalue;
  18. return _node;
  19. }
  20. //由區間[left,right]建立一棵線段樹
  21. Node *Build(Node *_root,int left,int right){
  22. _root = Init(_root,left,right); //節點初始化
  23. if(left != right){ //遞歸終止條件,表示已經線段樹建立到了葉節點
  24. _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);
  25. _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);
  26. }
  27. return _root; //回溯至最後一層時,則返回整棵樹的根節點
  28. }
  29. //中序遍歷,便於從左往右查看各節點存儲的線段區間
  30. void inorder(Node *_node){
  31. if(_node){
  32. inorder(_node -> lchild);
  33. printf("第%d個遍歷的節點,存儲區間為:[%d,%d]\n",flag++,_node -> leftvalue,_node -> rightvalue);
  34. inorder(_node -> rchild);
  35. }
  36. }
  37. /**用於查詢一個給定的區間是由線段樹中哪幾個子區間構成的,為了簡化代碼,
  38. **因此保證輸入的區間範圍合法,這裏就不作輸入的合法性判斷了
  39. */
  40. void Query(Node *_node,int left,int right){
  41. /**要查詢一個給定的區間是由線段樹中哪幾個子區間構成的
  42. **首先要滿足的是當前遍歷到的區間要和待查詢區間有交集,
  43. **否則的話不再繼續往當前節點的孩子節點繼續遍歷,原因很簡單
  44. **每個孩子節點存儲的區間範圍都是包含於父親節點的,父親節點與
  45. **待查詢區間無交集的話,則孩子節點一定無交集
  46. **/
  47. if(right >= _node -> leftvalue && left <= _node -> rightvalue){
  48. /**如果當前遍歷到的線段樹區間完全屬於待查詢區間,
  49. **那麽選取該區間,否則的話,繼續縮小範圍,
  50. **在當前節點的左孩子和右孩子節點繼續尋找
  51. **/
  52. if(left <= _node -> leftvalue && right >= _node -> rightvalue){
  53. printf("[%d,%d]\n",_node -> leftvalue,_node -> rightvalue);
  54. }
  55. else{
  56. Query(_node -> lchild,left,right);
  57. Query(_node -> rchild,left,right);
  58. }
  59. }
  60. }
  61. int main(){
  62. Node *root;
  63. root = Build(root,1,10);
  64. inorder(root);
  65. printf("區間[2,7]由線段樹中以下區間構成:\n");
  66. Query(root,2,7);
  67. return 0;
  68. }


我們以區間[2,7]為例,得出以下運行結果:

技術分享

(2).區間修改操作:

在這裏我們依然以RMQ問題為例,假如一開始的時候,線段樹中每個節點的權值都是1,那麽現在我要做的是,指定一個合法

的區間,然後對這個區間內所有的數加上或者減去某個數,如果我們按照區間的內的數一一的去遍歷並修改線段樹的節點的話,那

麽改動的節點數必然遠遠超過O(logn)個,而且會存在大量的重復遍歷操作,那麽要怎麽樣才能提高程序的效率呢?

首先,我們考慮給定的修改區間,按照前面我們討論過的問題,我們可以把待操作區間變成幾個相連的子區間,那麽我們試

想,當我們要修改一個給定區間時,我們對其所有子區間進行修改,這樣的話不就把整個待修改區間處理完畢了嗎?這樣的話

我們是否可以只通過修改幾個子區間節點的值,而不考慮它們的孩子節點,就完成所有的操作了呢?

實際上,如果不考慮這些子區間的孩子節點的話,是錯誤的,因為在父親節點所帶的權值發生變化時,比如說上圖示中區間

[1,2]中每個值都加上5,那麽我們把線段樹中表示區間[1,2]的節點修改完畢是否就可以了呢?答案顯然是錯誤的,因為該節點的左孩

子([1,1])和右孩子節點所表示的區間([2,2])中的值也都發生了變化。

所以在這裏我們為了方便,我們在節點定義中加入一個標記的量,用來存儲對節點的修改情況。顯然,當我們自上而下的訪

某節點時,父親節點的標記要"傳給"孩子節點,即修改大的區間,其子區間也必然被改動。

有了以上的分析,我們可以總結操作:

首先找到樹中哪幾個節點表示的區間,能夠組成我們待修改的區間,然後從這些節點開始向下遍歷,將以這些節點為根節點

的子樹節點權值做相應的改變。(邊查找對應子區間,邊修改權值)

完整代碼如下:

[cpp] view plain copy
  1. #include<cstdio>
  2. #include<cstdlib>
  3. typedef struct node Node;
  4. struct node{
  5. int leftvalue;
  6. int rightvalue; //分別用來記錄節點記錄的區間範圍
  7. Node *lchild; //左孩子節點
  8. Node *rchild; //右孩子節點
  9. int weight; //表示節點的權值
  10. int mark; //表示當前節點改變的值(權重加減處理)
  11. };
  12. int flag = 1; //用於標記當前遍歷到哪個節點
  13. //節點的初始化
  14. Node *Init(Node *_node,int lvalue,int rvalue){
  15. _node = (Node *)malloc(sizeof(Node));
  16. _node -> lchild = NULL;
  17. _node -> rchild = NULL;
  18. _node -> leftvalue = lvalue;
  19. _node -> rightvalue = rvalue;
  20. _node -> weight = 1; //為了方便,一開始的時候,線段樹每個節點的權值都設為1
  21. _node -> mark = 0; //初始狀態時,所有節點的權重均為1,因此一開始的時候,線段樹每個節點的標記均為0
  22. return _node;
  23. }
  24. //由區間[left,right]建立一棵線段樹
  25. Node *Build(Node *_root,int left,int right){
  26. _root = Init(_root,left,right); //節點初始化
  27. if(left != right){ //遞歸終止條件,表示已經線段樹建立到了葉節點
  28. _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);
  29. _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);
  30. }
  31. return _root; //回溯至最後一層時,則返回整棵樹的根節點
  32. }
  33. //中序遍歷,便於從左往右查看各節點存儲的線段區間
  34. void inorder(Node *_node){
  35. if(_node){
  36. inorder(_node -> lchild);
  37. printf("\n第%d個遍歷的節點,存儲區間為:[%d,%d]\n",flag,_node -> leftvalue,_node -> rightvalue);
  38. printf("\n第%d個遍歷的節點,權值為%d,標記為%d\n",flag++,_node -> weight,_node -> mark);
  39. inorder(_node -> rchild);
  40. }
  41. }
  42. /**用於查詢一個給定的區間是由線段樹中哪幾個子區間構成的,為了簡化代碼,
  43. **因此保證輸入的區間範圍合法,這裏就不作輸入的合法性判斷了
  44. */
  45. void Query(Node *_node,int left,int right){
  46. /**要查詢一個給定的區間是由線段樹中哪幾個子區間構成的
  47. **首先要滿足的是當前遍歷到的區間要和待查詢區間有交集,
  48. **否則的話不再繼續往當前節點的孩子節點繼續遍歷,原因很簡單
  49. **每個孩子節點存儲的區間範圍都是包含於父親節點的,父親節點與
  50. **待查詢區間無交集的話,則孩子節點一定無交集
  51. **/
  52. if(right >= _node -> leftvalue && left <= _node -> rightvalue){
  53. /**如果當前遍歷到的線段樹區間完全屬於待查詢區間,
  54. **那麽選取該區間,否則的話,繼續縮小範圍,
  55. **在當前節點的左孩子和右孩子節點繼續尋找
  56. **/
  57. if(left <= _node -> leftvalue && right >= _node -> rightvalue){
  58. printf("[%d,%d]\n",_node -> leftvalue,_node -> rightvalue);
  59. }
  60. else{
  61. Query(_node -> lchild,left,right);
  62. Query(_node -> rchild,left,right);
  63. }
  64. }
  65. }
  66. /**對指定區間的值進行增添操作,顯然,當某個子區間的值進行修改了之後
  67. **以該節點為根節點的子樹區間的值均要修改
  68. **/
  69. void change(Node *node){
  70. if(node){
  71. if(node -> lchild){
  72. node -> lchild -> mark += node -> mark;
  73. node -> lchild -> weight += node -> lchild -> mark;
  74. change(node -> lchild);
  75. }
  76. if(node -> rchild){
  77. node -> rchild -> mark += node -> mark;
  78. node -> rchild -> weight += node -> rchild -> mark;
  79. change(node -> rchild);
  80. }
  81. }
  82. }
  83. /**更改某個區間的權值,整棵線段樹節點值的變化為了簡化代碼,
  84. **因此保證輸入的區間範圍合法,這裏就不作輸入的合法性判斷,pos表示增減操作的值
  85. **/
  86. void update(Node *node,int left,int right,int pos){
  87. //先查找待處理區間的組成區間,再修改區間的權值
  88. if(right >= node -> leftvalue && left <= node -> rightvalue){
  89. if(left <= node -> leftvalue && right >= node -> rightvalue){
  90. node -> mark = pos;
  91. node -> weight += node -> mark;
  92. //修改以該節點為根的子樹所有節點的權值和標記
  93. change(node);
  94. }
  95. else{
  96. update(node -> lchild,left,right,pos);
  97. update(node -> rchild,left,right,pos);
  98. }
  99. }
  100. }
  101. int main(){
  102. Node *root;
  103. root = Build(root,1,4);
  104. //[1,3]中每個數都加上5;
  105. update(root,1,3,5);
  106. inorder(root);
  107. return 0;
  108. }


運行結果:

技術分享

線段樹初學,有錯誤和不足之處還望指正,O(∩_∩)O謝謝,後續在深入學習的過程中,還會增加更多的關於線段樹的文章,

者對本文中的應用範圍再進行擴充。

線段樹(二)