線段樹入門

一、定義

         線段樹(Segment Tree)是一棵完全二叉樹。從他的名字可知,樹中每個節點都代表一個線段,或者說一個區間。事實上,樹的根節點代表整體區間,左右子樹分別代表左右子區間。一個典型的線段樹如下圖所示:

                                                                       

          線段樹主要有三個性質:

            (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;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章