【转】线段树入门

一、定义

         线段树(Segment Tree)是一棵完全二叉树。从他的名字可知,树中每个节点都代表一个线段,或者说一个区间。事实上,树的根节点代表整体区间,左右子树分别代表左右子区间。一个典型的线段树如下图所示:

                                                                        

          线段树主要有三个性质:

            (1)长度范围为[1,L]的一棵线段树的的深度不超过log2(L-1)+1 (根节点深度为0)。

           (2)线段树上的节点个数不超过2L个。

           (3)线段树把区间上的任意一条线段都分成不超过2log2(L)段。

       线段树的结构在这,可是有啥用呢?通常,树上每个节点都维护相应区间的一些性质,如区间最大值,最小值,区间和等,这些维护的信息才是重头戏。

二、操作

(1)创建线段树:

        由线段树的结构,很容易构造出树节点的结构,并且写出线段树的递归构造算法。

        树节点:

 

  1. <span style="font-size:12px;">class Node{  
  2.     int left,right;  
  3.     Node leftchild;  
  4.     Node rightchild;  
  5.     int minval;  // 区间的最小值  
  6.     int maxval;  // 区间的最大值  
  7.     int sum;     // 区间和  
  8.     int delta;   // 区间延时标记  
  9.     Node(int left,int right)  
  10.     {  
  11.         this.left=left;  
  12.         this.right=right;  
  13.         this.sum=0;   
  14.         this.delta=0;    
  15.         leftchild=null;  
  16.         rightchild=null;  
  17.     }  
  18. }</span>  

        创建算法:

 

 

  1. <span style="font-size:12px;">public void buildSegTree(Node root)  
  2. {  
  3.      if(root.left==root.right){  
  4.         root.minval=num[root.left];  
  5.         root.maxval=num[root.left];  
  6.         root.sum=num[root.left];  
  7.         return;  
  8.     }  
  9.     int mid=root.left+(root.right-root.left)/2;  
  10.     Node left=new Node(root.left,mid);  
  11.     Node right=new Node(mid+1,root.right);  
  12.     root.leftchild=left;  
  13.     root.rightchild=right;  
  14.     buildSegTree(root.leftchild);  
  15.     buildSegTree(root.rightchild);  
  16.     root.minval=min(root.leftchild.minval,root.rightchild.minval);  
  17.     root.maxval=max(root.leftchild.maxval,root.rightchild.maxval);  
  18.     root.sum=root.leftchild.sum+root.rightchild.sum;  
  19. }</span>  

(2)修改和查询:

        对线段树的操作主要有两种,一种是点修改和对应的查询,另一种是区间修改和对应的查询。

      (2.1)点修改问题的一个典型例子是RMQ(范围最小值问题),给定有n个元素的数组A1、A2……An,定义以下两操作:

       Update(x,v):把Ax修改为V

       Query(L,R):计算min{AL,AL+1,……AR}

       若只对数组进行线性维护,每次用一个循环来计算最小值,时间复杂度是线性的。下面来看看在线段树上进行维护的方法。

       更新线段树时,显然要更新线段[x,x]对应的节点的信息(最小值),而线段[x,x]中的最小值就是更改后的v,然后还要更新所有祖

先节点的信息,祖先节点的最小值可以由左右子孙节点的最小值综合得到。这样,递归的算法如下:

 

  1. <span style="font-size:12px;">void update(Node root,int pos,int res)         //将pos位置的值更新为res  
  2. {  
  3.     int mid=root.left+(root.right-root.left)/2;  
  4.     if(root.right==root.left)              //叶节点,直接更新pos  
  5.     {   root.minval=res;               //最小值  
  6.         root.maxval=res;               //最大值  
  7.         root.sum=res;                  //区间和  
  8.         return;  
  9.     }  
  10.     else{  
  11.         if(pos<=mid)                  //先递归更新左子树或右子树  
  12.             update(root.leftchild,pos,res);  
  13.         else  
  14.             update(root.rightchild,pos,res);  
  15.         root.minval=min(root.leftchild.minval,root.rightchild.minval);     //最后计算本节点的信息  
  16.         root.maxval=max(root.leftchild.maxval,root.leftchild.maxval);  
  17.         root.sum=root.leftchild.sum+root.rightchild.sum;  
  18.     }     
  19. <span style="font-size:14px;">}</span></span>  

可以看出,更新的时间复杂度为O(logn)。

 

       在查询时,沿着根节点从上到下找到待查询线段的左边界和右边界,则夹在中间的所有叶子节点不重复地覆盖了整个查询线段。举

个例子,查询[0,3]段的最小值。从根节点开始向下查找,最后落到[0,2]和[3,3]两个节点上,整段的最小值可由这两个子段综合得到。

       查询时,树的左右各有一条“主线”,每层最多有两个节点向下衍伸,因此最后查询到的子节点不超过2h个(性质3)。这其实就是将查询线段分解为不超过2h个不相交的并。再通过这些子区间的信息得到整体区间的信息。代码如下:

 

  1. <span style="font-size:12px;">int query(Node root,int l,int r)  
  2. {  
  3.     if(l<=root.left&&r>=root.right)   //区间[l,r]将目前查询的节点范围包括进去了  
  4.     {  
  5.         return root.minval;  
  6.     }  
  7.     int ans=Integer.MAX_VALUE;  
  8.     int mid=root.left+(root.right-root.left)/2;  
  9.     if(l<=mid) ans=min(ans,query(root.leftchild,l,r));  //当前节点没有被[l,r]包括,但是[l,r]和节点左半部分有交集  
  10.     if(r>mid) ans=min(ans,query(root.rightchild,l,r));  //当前节点没被[l,r]包括,但是[l,r]和节点右半部分有交集  
  11.     return ans;  
  12. }</span>  

可以看出,查询操作的时间复杂度为O(logn)。

 

      (2.2)区间修改查询问题,要比点修改复杂一些,定义如下两个操作:

       Add(L,R,v):把AL、AL+1……AR的值全部加上v

       Query(L,R):计算AL、AL+1……AR区间和

       点修改最多修改logn个节点,但是区间修改最坏情况下会影响到数中的所有节点。这里要意识到,其实区间修改也是针对区间的操作。可以使用上面查询区间的思想,将一个区间分成多个子区间,再对每个子区间进行修改,而这些子区间覆盖的子区间节点则不进行修改。这样可以保证修改的时间复杂度也为O(logn)。

       这些选定的子节点被更改后,其父亲节点的信息只要简单综合,就能得到正确的修改。但是,其子节点的信息却没有得到更改。

       还是用开头那个图的来举例子:

       (1).给[0,3]区间里的每个值增加1。从根节点遍历下去,可以选取[0,2]和[3,3],对这两个区间维护的区间和进行修改。[3,4]的区间和可以得到正确更新([3,3]+[4,4]),进一步,[0,4]的区间和也可以得到更新[0,2]+[3,4]。

       (2).在(1)的操作基础上,给[2,3]区间中的每个值加1,还是从根节点往下找,会找到[2,2]和[3,3]这两个区间,其中[3,3]区间中的值直接加1就可以了。但是,[2,2]区间中的值要加2,因为在(1)中,只更新了[0,2],更新信息没有在[2,2]和[0,0]中体现出来。

       这里,要引入非常关键的延时标记,其关键思想是:我决定更新一个选定的区间,但子区间先不更新。等到要访问子区间的时候,再将父亲区间中累计的更新应用到子区间上。

       这需要在每个节点上维护一个延时标记,每次更新操作都要记录到其中。若是下次要访问子节点,需要将父亲节点的更改累计到左右子节点的延时标记上,并根据父亲节点的延时标记对左右子节点的信息进行更改。之所以要将父节点的更改累计而不是直接替换到左右子节点上,是因为可能有其他的操作对子节点进行了更新。若是直接替换,可能将其他操作的更新覆盖了,下次访问子节点的子节点时,会产生不正确的更新。

 

  1. void pushDown(Node root)  
  2. {  
  3.     if(root.delta>0)  
  4.     {  
  5.         root.leftchild.delta+=root.delta;  
  6.         root.rightchild.delta+=root.delta;  
  7.         root.leftchild.sum+=(root.leftchild.right-root.leftchild.left+1)*root.delta;  
  8.         root.rightchild.sum+=(root.rightchild.right-root.rightchild.left+1)*root.delta;  
  9.         root.delta=0;  
  10.     }  
  11. }  
  12. void matain(Node root)  
  13. {  
  14.     root.sum=root.leftchild.sum+root.rightchild.sum;  
  15. }  
  16. void segmentAdd(Node root,int l,int r,int a)  
  17. {  
  18.     if(root.left>=l&&root.right<=r)  
  19.     {  
  20.         root.delta+=a;  
  21.         root.sum+=(root.right-root.left+1)*a;  
  22.         return;  
  23.     }  
  24.     pushDown(root);               //要访问子节点,若本节点延时标记有记录之前的更新,将信息传递到子节点中  
  25.     int mid=root.left+(root.right-root.left)/2;  
  26.     if(l<=mid) segmentAdd(root.leftchild,l,r,a);   
  27.     if(r>mid) segmentAdd(root.rightchild,l,r,a);  
  28.     matain(root);                 //回头更新本节点信息  
  29. }  

 

       针对区间修改的查询和点修改的查询的基本思想一样。只是要注意,节点延时标记信息的传递。例如,上例中(1)将[0,2]区间的值加1,紧接着查询[2,2]区间的区间和。这时就要注意将[0,2]节点中的延时标记传递到[2,2]和[0,0]中。

 

    1. int segmentQuery(Node root,int l,int r)  
    2. {  
    3.     if(l<=root.left&&r>=root.right)  
    4.     {  
    5.         return root.sum;  
    6.     }  
    7.     pushDown(root);  
    8.     int mid=root.left+(root.right-root.left)/2;  
    9.     int sum=0;  
    10.     if(l<=mid) sum+=segmentQuery(root.leftchild,l,r);  
    11.     if(r>mid)  sum+=segmentQuery(root.rightchild,l,r);  
    12.     return sum;  
    13. }  
posted @ 2014-09-28 10:09  Pacific-hong  阅读(110)  评论(0编辑  收藏  举报