线段树入门

一、定义

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

                                                                        线段树入门_第1张图片

          线段树主要有三个性质:

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

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

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

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

二、操作

(1)创建线段树:

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

        树节点:

class Node{
	int left,right;
	Node leftchild;
	Node rightchild;
	int minval;	 // 区间的最小值
	int maxval;	 // 区间的最大值
	int sum;	 // 区间和
	int delta;	 // 区间延时标记
	Node(int left,int right)
	{
		this.left=left;
		this.right=right;
		this.sum=0;	
		this.delta=0;  
		leftchild=null;
		rightchild=null;
	}
}
         创建算法:

public void buildSegTree(Node root)
{
	 if(root.left==root.right){
		root.minval=num[root.left];
		root.maxval=num[root.left];
		root.sum=num[root.left];
		return;
	}
	int mid=root.left+(root.right-root.left)/2;
	Node left=new Node(root.left,mid);
	Node right=new Node(mid+1,root.right);
	root.leftchild=left;
	root.rightchild=right;
	buildSegTree(root.leftchild);
	buildSegTree(root.rightchild);
	root.minval=min(root.leftchild.minval,root.rightchild.minval);
	root.maxval=max(root.leftchild.maxval,root.rightchild.maxval);
	root.sum=root.leftchild.sum+root.rightchild.sum;
}
(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,然后还要更新所有祖

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

void update(Node root,int pos,int res)         //将pos位置的值更新为res
{
	int mid=root.left+(root.right-root.left)/2;
	if(root.right==root.left)              //叶节点,直接更新pos
	{ 	root.minval=res;               //最小值
		root.maxval=res;               //最大值
		root.sum=res;                  //区间和
		return;
	}
	else{
		if(pos<=mid)                  //先递归更新左子树或右子树
			update(root.leftchild,pos,res);
		else
			update(root.rightchild,pos,res);
		root.minval=min(root.leftchild.minval,root.rightchild.minval);     //最后计算本节点的信息
		root.maxval=max(root.leftchild.maxval,root.leftchild.maxval);
		root.sum=root.leftchild.sum+root.rightchild.sum;
	}	
}
可以看出,更新的时间复杂度为O(logn)。

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

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

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

int query(Node root,int l,int r)
{
	if(l<=root.left&&r>=root.right)   //区间[l,r]将目前查询的节点范围包括进去了
	{
		return root.minval;
	}
	int ans=Integer.MAX_VALUE;
	int mid=root.left+(root.right-root.left)/2;
	if(l<=mid) ans=min(ans,query(root.leftchild,l,r));  //当前节点没有被[l,r]包括,但是[l,r]和节点左半部分有交集
	if(r>mid) ans=min(ans,query(root.rightchild,l,r));  //当前节点没被[l,r]包括,但是[l,r]和节点右半部分有交集
	return ans;
}
可以看出,查询操作的时间复杂度为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]中体现出来。

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

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


void pushDown(Node root)
{
	if(root.delta>0)
	{
		root.leftchild.delta+=root.delta;
		root.rightchild.delta+=root.delta;
		root.leftchild.sum+=(root.leftchild.right-root.leftchild.left+1)*root.delta;
		root.rightchild.sum+=(root.rightchild.right-root.rightchild.left+1)*root.delta;
		root.delta=0;
	}
}
void matain(Node root)
{
	root.sum=root.leftchild.sum+root.rightchild.sum;
}
void segmentAdd(Node root,int l,int r,int a)
{
	if(root.left>=l&&root.right<=r)
	{
		root.delta+=a;
		root.sum+=(root.right-root.left+1)*a;
		return;
	}
	pushDown(root);               //要访问子节点,若本节点延时标记有记录之前的更新,将信息传递到子节点中
	int mid=root.left+(root.right-root.left)/2;
	if(l<=mid) segmentAdd(root.leftchild,l,r,a); 
	if(r>mid) segmentAdd(root.rightchild,l,r,a);
	matain(root);                 //回头更新本节点信息
}

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

int segmentQuery(Node root,int l,int r)
{
	if(l<=root.left&&r>=root.right)
	{
		return root.sum;
	}
	pushDown(root);
	int mid=root.left+(root.right-root.left)/2;
	int sum=0;
	if(l<=mid) sum+=segmentQuery(root.leftchild,l,r);
	if(r>mid)  sum+=segmentQuery(root.rightchild,l,r);
	return sum;
}


你可能感兴趣的:(线段树入门)