線段樹(二)

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

 

1.線段樹的定義:web

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

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

 

舉例描述:url

所以有了以上對線段樹的定義,咱們能夠將區間[1,10]的線段樹結構描述出來:spa

圖片來自於百度百科.net

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

 

2.構造線段樹blog

顯然,當咱們將線段樹定義清楚以後,那麼咱們就要想要怎麼去實現它。遞歸

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

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

有了以上的思路,咱們能夠得出如下構造線段樹的代碼:

 

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


爲了檢驗構造狀況是否和上述圖示一致,咱們能夠利用樹的中序遍歷,查看每一個節點存儲的線段,所以咱們得出如下完整代碼:

 

 

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

 

 

運行結果:

咱們發現,存儲的結果與一開始定義的徹底一致,因而咱們便成功的創建好了一棵空的線段樹。

 

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

 

(1).區間查詢問題:

RMQ(Range Minimum/Maximum Query),即區間最值查詢,是指這樣一個問題:對於長度爲n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在i,j之間的最小/大值。

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


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


 

運行結果:

相關文章
相關標籤/搜索