數據結構----伸展樹

伸展樹SPlay tree

只要理解了AVL的旋轉過程,伸展樹基本就能夠明白的了,伸展樹有個神奇之處在於把訪問的節點旋轉至根節點,可以實現區間刪除,尋找前驅後驅等等。。。感覺伸展樹比那些主席樹,劃分樹作用更大似的,因爲我能夠知道用伸展樹來幹嘛,而其他數除了求第K大的值之外不知拿來幹嘛,說到底還是做題太少,見識太少。,。需要好好熟悉理解Splay的代碼。。。。。大牛都說簡單,然而讓我自己默寫都寫不出來QAQ


進入正題:

參考鏈接:

1點擊打開鏈接

數據結構之伸展樹

伸展樹(Splay tree)學習小結

HNOI2002]營業額統計 Splay tree 

參考論文:

1)     楊思雨《伸展樹的基本操作與應用》

(2)     Crash《運用伸展樹解決數列維護問題》


首先要理解旋轉方式:一共有六種,鏡像3種,其實就是理解3種就能夠明白了。(zig 右旋     zag 左旋)

x   目標結點   

y   x的父結點

z   y的父結點

1.     當y爲根結點,進行一次zig或zag

2      y不是根結點,當x,y同時是各自父結點的左子樹或右子樹,進行兩次zig或zag (可以自頂向下,也可以自底向上)

3     y不是根結點,當x,y其中一個是父結點的左子樹,另一個是右子樹,進行一次zag,一次zig


代碼中的旋轉函數比較難理解,需要會畫圖就容易理解了,記住需要左旋的圖和右旋的圖,

代碼中的示例圖:


① 爲代碼中的ch[y][!kind]=ch[x][kind];              ②pre[ch[x][kind]]=y;  (當kind=0的情況,kind=1類似)


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1、Splay 概述

二叉查找樹(Binary Search Tree,也叫二叉排序樹,即Binary Sort Tree)能夠支持多種動態集合操作,它可以用來表示有序集合、建立索引等,因而在實際應用中,二叉排序樹是一種非常重要的數據結構。

從算法複雜度角度考慮,我們知道,作用於二叉查找樹上的基本操作(如查找,插入等)的時間複雜度與樹的高度成正比。對一個含n個節點的完全二叉樹,這些操作的最壞情況運行時間爲O(log n)。但如果因爲頻繁的刪除和插入操作,導致樹退化成一個n個節點的線性鏈(此時即爲一個單鏈表),則這些操作的最壞情況運行時間爲O(n)。爲了克服以上缺點,很多二叉查找樹的變形出現了,如紅黑樹、AVL樹,Treap樹等。

本文介紹了二叉查找樹的一種改進數據結構–伸展樹(Splay Tree)。它的主要特點是不會保證樹一直是平衡的,但各種操作的平攤時間複雜度是O(log n),因而,從平攤複雜度上看,二叉查找樹也是一種平衡二叉樹。另外,相比於其他樹狀數據結構(如紅黑樹,AVL樹等),伸展樹的空間要求與編程複雜度要小得多。

2、 基本操作

伸展樹的出發點是這樣的:考慮到局部性原理(剛被訪問的內容下次可能仍會被訪問,查找次數多的內容可能下一次會被訪問),爲了使整個查找時間更小,被查頻率高的那些節點應當經常處於靠近樹根的位置。這樣,很容易得想到以下這個方案:每次查找節點之後對樹進行重構,把被查找的節點搬移到樹根,這種自調整形式的二叉查找樹就是伸展樹。每次對伸展樹進行操作後,它均會通過旋轉的方法把被訪問節點旋轉到樹根的位置。

爲了將當前被訪問節點旋轉到樹根,我們通常將節點自底向上旋轉,直至該節點成爲樹根爲止。“旋轉”的巧妙之處就是在不打亂數列中數據大小關係(指中序遍歷結果是全序的)情況下,所有基本操作的平攤複雜度仍爲O(log n)。

伸展樹主要有三種旋轉操作,分別爲單旋轉,一字形旋轉和之字形旋轉。爲了便於解釋,我們假設當前被訪問節點爲X,X的父親節點爲Y(如果X的父親節點存在),X的祖父節點爲Z(如果X的祖父節點存在)。

(1)    單旋轉

節點X的父節點Y是根節點。這時,如果X是Y的左孩子,我們進行一次右旋操作;如果X 是Y 的右孩子,則我們進行一次左旋操作。經過旋轉,X成爲二叉查找樹T的根節點,調整結束。



(2)    一字型旋轉

節點X 的父節點Y不是根節點,Y 的父節點爲Z,且X與Y同時是各自父節點的左孩子或者同時是各自父節點的右孩子。這時,我們進行一次左左旋轉操作或者右右旋轉操作。

(3)    之字形旋轉

節點X的父節點Y不是根節點,Y的父節點爲Z,X與Y中一個是其父節點的左孩子而另一個是其父節點的右孩子。這時,我們進行一次左右旋轉操作或者右左旋轉操作。

3、伸展樹區間操作

在實際應用中,伸展樹的中序遍歷即爲我們維護的數列,這就引出一個問題,怎麼在伸展樹中表示某個區間?比如我們要提取區間[a,b],那麼我們將a前面一個數對應的結點轉到樹根,將b 後面一個結點對應的結點轉到樹根的右邊,那麼根右邊的左子樹就對應了區間[a,b]。原因很簡單,將a 前面一個數對應的結點轉到樹根後, a 及a 後面的數就在根的右子樹上,然後又將b後面一個結點對應的結點轉到樹根的右邊,那麼[a,b]這個區間就是下圖中B所示的子樹。

利用區間操作我們可以實現線段樹的一些功能,比如回答對區間的詢問(最大值,最小值等)。具體可以這樣實現,在每個結點記錄關於以這個結點爲根的子樹的信息,然後詢問時先提取區間,再直接讀取子樹的相關信息。還可以對區間進行整體修改,這也要用到與線段樹類似的延遲標記技術,即對於每個結點,額外記錄一個或多個標記,表示以這個結點爲根的子樹是否被進行了某種操作,並且這種操作影響其子結點的信息值,當進行旋轉和其他一些操作時相應地將標記向下傳遞。

與線段樹相比,伸展樹功能更強大,它能解決以下兩個線段樹不能解決的問題:

(1) 在a後面插入一些數。方法是:首先利用要插入的數構造一棵伸展樹,接着,將a 轉到根,並將a 後面一個數對應的結點轉到根結點的右邊,最後將這棵新的子樹掛到根右子結點的左子結點上。

(2)  刪除區間[a,b]內的數。首先提取[a,b]區間,直接刪除即可。


5、 應用

(1)     數列維護問題

題目:維護一個數列,支持以下幾種操作:

1. 插入:在當前數列第posi 個數字後面插入tot 個數字;若在數列首位插入,則posi 爲0。

2. 刪除:從當前數列第posi 個數字開始連續刪除tot 個數字。

3. 修改:從當前數列第posi 個數字開始連續tot 個數字統一修改爲c 。

4. 翻轉:取出從當前數列第posi 個數字開始的tot 個數字,翻轉後放入原來的位置。

5. 求和:計算從當前數列第posi 個數字開始連續tot 個數字的和並輸出。

6. 求和最大子序列:求出當前數列中和最大的一段子序列,並輸出最大和。

(2)     輕量級web服務器lighttpd中用到數據結構splay tree.


例題:

[HNOI2002]營業額統計


[HNOI2002]營業額統計

Time Limit: 5 Sec  Memory Limit: 162 MB
Submit: 4128  Solved: 1305
[Submit][Status][Discuss]

Description

營業額統計 Tiger最近被公司升任爲營業部經理,他上任後接受公司交給的第一項任務便是統計並分析公司成立以來的營業情況。 Tiger拿出了公司的賬本,賬本上記錄了公司成立以來每天的營業額。分析營業情況是一項相當複雜的工作。由於節假日,大減價或者是其他情況的時候,營業額會出現一定的波動,當然一定的波動是能夠接受的,但是在某些時候營業額突變得很高或是很低,這就證明公司此時的經營狀況出現了問題。經濟管理學上定義了一種最小波動值來衡量這種情況: 該天的最小波動值 當最小波動值越大時,就說明營業情況越不穩定。 而分析整個公司的從成立到現在營業情況是否穩定,只需要把每一天的最小波動值加起來就可以了。你的任務就是編寫一個程序幫助Tiger來計算這一個值。 第一天的最小波動值爲第一天的營業額。  輸入輸出要求

Input

第一行爲正整數 ,表示該公司從成立一直到現在的天數,接下來的n行每行有一個正整數 ,表示第i天公司的營業額。

Output

輸出文件僅有一個正整數,即Sigma(每天最小的波動值) 。結果小於2^31 。

Sample Input

6
5
1
2
5
4
6

Sample Output

12

HINT

結果說明:5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12


這題的數據本來有bug,少了一個數據,所有代碼中用到了if(scanf("%d",&num)==EOF) num=0;
但是這題的bug改完了,所以直接scanf就行了,不過學習了一下有bug得數據的做法
#include<cstdio>
#include<iostream>
#include <algorithm>
#include <map>
#include <cmath>
using namespace std;
#define N 100005
#define inf 1<<29
int pre[N],key[N],ch[N][2],root,tot1;   //分別表示父結點,鍵值,左右孩子(0爲左孩子,1爲右孩子),根結點,結點數量
int n;
//新建一個結點
void newNode(int &r,int father,int k)
{
	r=++tot1;
	pre[r]=father;
	key[r]=k;
	ch[r][0]=ch[r][1]=0;      //左右孩子爲空

}
//旋轉,kind爲1爲右旋,kind爲0爲左旋
/*  
    這裏一開始很難明白,感覺很亂,但是其實這裏就是旋轉的過程,畫圖就比較容易理解了
	kind一開始會有些亂,特別是kind即代表旋轉方向又表示左右節點,放在一起很亂
	但這就是大牛的厲害之處吧,簡潔,但是熟悉明白還需要一段時間 
*/ 
void Rotate(int x,int kind)
{
	int y=pre[x];
	//類似SBT,要把其中一個分支先給父節點
	ch[y][!kind]=ch[x][kind];
	pre[ch[x][kind]]=y;
	//如果父節點不是根結點,則要和父節點的父節點連接起來
	if(pre[y])
	   ch[pre[y]][ch[pre[y]][1]==y]=x;
	pre[x]=pre[y];
	ch[x][kind]=y;
	pre[y]=x;
}
//Splay調整,將根爲r的子樹調整爲goal
void Splay(int r,int goal)
{
	while(pre[r]!=goal)
	{
		//父節點即是目標位置,goal爲0表示,父節點就是根結點
		if(pre[pre[r]]==goal)
		    Rotate(r,ch[pre[r]][0]==r);
		else
		{
			int y=pre[r];
			int kind=ch[pre[y]][0]==y;
			//兩個方向不同,則先左旋再右旋
			if(ch[y][kind]==r)
			{
				Rotate(r,!kind);
				Rotate(r,kind);
			}
			//兩個方向相同,相同方向連續兩次
			else
			{
				Rotate(y,kind);
				Rotate(r,kind);
			}
		}
	}
	//更新根結點
	if(goal==0) root=r;
}

int Insert(int k)
{
	int r=root;
	while(ch[r][key[r]<k])
	{
		//不重複插入,這裏應該是相對於這道題來講的吧,相同值無需再插入 
		if(key[r]==k){
			Splay(r,0);
			return 0;
		}
		r=ch[r][key[r]<k];
	}
	newNode(ch[r][key[r]<k],r,k);
	//將新插入的結點更新至根結點
	Splay(ch[r][key[r]<k],0);
	return 1;
}
//找前驅,即左子樹的最右結點
int get_pre(int x)
{
	int tmp=ch[x][0];
	if(tmp==0) return inf;
	while(ch[tmp][1])
	    tmp=ch[tmp][1];
	return key[x]-key[tmp];
}
//找後繼,即右子樹的最左結點
int get_next(int x)
{
	int tmp=ch[x][1];
	if(tmp==0) return inf;
	while(ch[tmp][0])
	     tmp=ch[tmp][0];
	return key[tmp]-key[x];
}

int main(){

#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
#endif
    while(~scanf("%d",&n))
    {
    	root=tot1=0;
    	int ans=0;
    	for(int i=1;i<=n;i++)
    	{
    		int num;
    		//scanf("%d",&num);
    		//一開始數據有bug,所以這樣寫才能過,不過bug 改回來了 
			//把這裏改成一般輸入scanf("%d",&num)也可以過 
     		if(scanf("%d",&num)==EOF) num=0;   //讀到文件結尾 
     		if(i==1)
     		{
     			ans+=num;
     			newNode(root,0,num);
     			continue;
			}
			if(Insert(num)==0) continue;
			int a=get_pre(root);
			int b=get_next(root);
			ans+=min(a,b);
		}
		printf("%d\n",ans);
	}
    
}


發佈了86 篇原創文章 · 獲贊 38 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章