伸展樹SPlay tree
只要理解了AVL的旋轉過程,伸展樹基本就能夠明白的了,伸展樹有個神奇之處在於把訪問的節點旋轉至根節點,可以實現區間刪除,尋找前驅後驅等等。。。感覺伸展樹比那些主席樹,劃分樹作用更大似的,因爲我能夠知道用伸展樹來幹嘛,而其他數除了求第K大的值之外不知拿來幹嘛,說到底還是做題太少,見識太少。,。需要好好熟悉理解Splay的代碼。。。。。大牛都說簡單,然而讓我自己默寫都寫不出來QAQ
進入正題:
參考鏈接:
數據結構之伸展樹
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 MBSubmit: 4128 Solved: 1305
[Submit][Status][Discuss]
Description
Input
Output
Sample Input
5
1
2
5
4
6
Sample Output
HINT
結果說明:5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12
#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);
}
}