Splay Tree(伸展樹)詳解


Splay Tree(伸展樹)

簡介

Splay Tree是一種二叉查找樹(BST),即滿足二叉樹上任意一個節點的左兒子權值>自身權值>右兒子權值,它通過旋轉操作使得樹上單次操作的均攤復雜度為 \(\log n\),由Daniel Sleator和Robert Endre Tarjan(又是Tarjan)發明,希望了解復雜度證明的可以自行查詢資料(我不會證)

實現

存儲、維護與更新

一般我們把樹上的每一個節點都作為結構體存放,節點中維護當前節點對應的值,當前節點對應值的個數,兩個子節點的編號,子樹中節點的個數以及父節點編號

為了實現方便,常用 \(ch[0]\)\(son[0]\) 表示左兒子,用 \(ch[1]\)\(son[1]\) 表示右兒子

struct Node
{
    int v,cnt,siz,fa,ch[2];//節點值,個數,子樹及自身節點個數,父親,兒子 0->left 1->right 
};

實現中常常需要改變節點間的關系,我們需要從子節點更新當前節點的信息

void update(int x)
{
    node[x].siz=node[node[x].ch[0]].siz+node[node[x].ch[1]].siz+node[x].cnt;
}

旋轉

旋轉可以說是Splay的基礎,利用旋轉才能讓樹保持平衡,Splay中有左旋(Left Rotation)和右旋(Right Rotation),如下圖

image

不難發現,旋轉操作需要讓樹仍然滿足二叉查找樹的性質

具體地說,例如在這張圖中,右旋C節點我們就需要 1.用C替換B成為A的子節點 2.把C的右兒子作為B的左兒子 3.把B作為C的右兒子

左旋B節點我們就需要 1.用B替換C成為A的子節點 2.把B的左兒子作為C的右兒子 3.把C作為B的左兒子

在具體實現中,我們可以將左旋和右旋通過同一個函數實現,因為我們並不在意到底什么時候需要左旋什么時候需要右旋,只要每次旋轉都會將被旋轉的節點的深度減少 \(1\) 就能滿足我們的需求

不難發現被旋轉節點是一個左兒子還是右兒子以及被旋轉節點的父節點是一個左兒子還是右兒子都會對於旋轉操作產生影響,我們這里可以用一個變量 \(k\) 來表示x是不是y的右兒子,再來進行旋轉,建議在紙上照着下方的代碼畫一下旋轉的具體過程,有助於理解

void rotate(int x)
{
    int y=node[x].fa,z=node[y].fa,k=(node[y].ch[1]==x);//y是x的父親,z是y的父親,k表示x是不是y的右兒子
    node[z].ch[node[z].ch[1]==y]=x;//用x替換y作為z的兒子 
    node[x].fa=z;
    node[y].ch[k]=node[x].ch[k^1];//把x的對應兒子轉移給y 
    node[node[x].ch[k^1]].fa=y;
    node[x].ch[k^1]=y;//把y更新為x的兒子
    node[y].fa=x;
    update(y),update(x);//y是x的子節點所以必須先更新y再更新x
}

代碼中 ^1 表示異或1,其中 1^1=0 ,0^1=1,也就是取反的意思

伸展(Splay)

Splay操作即為把一個節點通過不斷旋轉旋轉到根,每次查詢或者修改操作后都將操作的節點Splay到根就能保證單次操作復雜度均攤為 \(\log n\)

但是我們不能單純地把節點不斷向上轉,考慮如下這種情況,我們想把C轉到根

image

我們發現如果單純把C向上轉的話,如果原本是鏈,轉動之后還是鏈,並不能優化,所以我們需要雙旋,也就是在當前節點和當前節點的父節點都是自己父節點的左兒子或者右兒子時,我們先轉動當前節點的父節點,再轉動當前節點,如下圖

image

這樣就讓單次操作復雜度均攤到了 \(\log n\),代碼實現如下

void splay(int x,int target)//讓x成為target的子節點,根節點是節點0的子節點
{
    while(node[x].fa!=target)//轉到目標就停止 
    {
        int y=node[x].fa,z=node[y].fa;
        if(z!=target)//如果轉一下就滿足就不多轉一次 
            ((node[z].ch[0]==y)^(node[y].ch[0]==x))?rotate(x):rotate(y);//三點共線就轉y否則轉x
        rotate(x);//轉一下x 
    }
    if(!target)Root=x;//如果是轉到根就更新根 
}

插入

插入一個新節點要先從根據插入節點與當前找到的節點的大小關系從根節點開始向兩個子節點走,直到找到插入節點值與當前節點相等,或者當前找到的節點編號不存在(即這個值從未出現過),然后更新信息即可

具體實現很好理解,看代碼

void insert(int x)//插入x 
{
    int cur=Root,from=0;//當前找到的節點編號cur以及節點來源from
    while(cur&&x!=node[cur].v)//找到了有相同值或者進入了未定義節點就停下 
        from=cur,cur=node[cur].ch[x>node[cur].v];//往子節點走 
    if(cur)//已經存在就增加個數即可 
        ++node[cur].cnt;
    else//創建新節點
    {
        cur=++node_cnt;//分配新編號 
        if(!from) Root=cur;//是根就更新根信息
        else node[from].ch[x>node[from].v]=cur;//不是根就更新父節點信息
        //更新新節點信息 
        node[cur].v=x;
        node[cur].cnt=1;
        node[cur].fa=from;
        node[cur].siz=1;
        node[cur].ch[0]=node[cur].ch[1]=0;
    }
    splay(cur,0);//轉到根 
}

查找

查找操作就是在樹上找一個值對應的節點,並且把這個節點轉到根,方便進行操作,不斷往下找就行,看代碼

void find(int x)//查找元素,調用后根即為查找的元素
{
    int cur=Root;//從根開始查 
    if(!cur)return;//樹為空就退出
    while(node[cur].ch[x>node[cur].v]&&x!=node[cur].v)
        //x不是當前節點值且當前節點有更小或更大值就進入子節點繼續找 
        cur=node[cur].ch[x>node[cur].v];//進入子節點 
    splay(cur,0);//將找到的節點轉到根 
}

查找前驅后繼

前驅定義為比一個數小的數中最大的,后繼定義為比一個數大的數中最小的

直接把要查找的值對應的節點轉到根,要查前驅就從左兒子開始一直往右找,要查后繼就從右兒子開始一直往左找,非常簡單

int find_pre_id(int x)//查前驅編號 
{
    find(x);//轉到根
    if(node[Root].v<x)return Root;//原樹中沒有這個值,就直接返回根 
    int cur=node[Root].ch[0];//進左子樹 
    while(node[cur].ch[1]) cur=node[cur].ch[1];//往右 
    return cur;
}
int find_nxt_id(int x)//查后繼編號 
{
    find(x);//轉到根
    if(node[Root].v>x)return Root;//原樹中沒有這個值,就直接返回根 
    int cur=node[Root].ch[1];//進右子樹 
    while(node[cur].ch[0]) cur=node[cur].ch[0];//往左 
    return cur;
}
int find_pre(int x)//查前驅值 
{
    x=find_pre_id(x);//找到前驅編號 
    return node[x].v;//返回值 
}
int find_nxt(int x)//查后繼值 
{
    x=find_nxt_id(x);//找到后繼編號 
    return node[x].v;//返回值 
}

查數的排名

排名定義為比一個數小的數的個數+1,只需要將這個數轉到根,返回比它左兒子的 \(size\) 即可(因為已經插入了極小值,所以不需要再多+1),如果沒有找到這個值,那么顯然當前的根節點就是這個數的前驅或者后繼,分類討論一下,如果是后繼那么顯然后繼的排名和當前值的排名沒有區別,如果是前驅那么自己的排名就是左兒子的 \(size\) 加上自己的 \(cnt\)

int get_rank(int x)
{
    find(x);//轉到根 
    //比它小的數的個數+1就是排名,這里因為我們插入了極小值就不用+1了 
    if(node[Root].v>=x)//根為后繼那么排名是一樣的
        return node[node[Root].ch[0]].siz;
	return node[node[Root].ch[0]].siz+node[Root].cnt;//根為前驅那么排名加上根的cnt就可以了
}

查排名對應的數

我們從根節點開始找,左邊和當前節點個數小於排名,就說明這個數一定在右子樹,我們把排名減去左邊和當前節點個數然后進入右子樹查詢新排名

如果左子樹節點更多就直接進左子樹查

否則當前找到的節點就是對應的數,返回即可

int kth(int rank)//查排名為k的數
{
    ++rank;//這里因為我們插入了極小值,排名需要+1 
    int cur=Root,son;//cur從根開始,son為當前節點的左兒子 
    if(node[cur].siz<rank) return -1;//沒有這么多數就退出 
    while(1)
    {
        son=node[cur].ch[0];
        if(rank>node[son].siz+node[cur].cnt)//左邊和當前節點個數不到k 
        {
            rank-=node[son].siz+node[cur].cnt;//減去這么多個 
            cur=node[cur].ch[1];//進入右子樹 
        }
        else if(node[son].siz>=rank) cur=son;//左子樹節點更多就進左子樹查
        else return node[cur].v;//找到了就返回 
    }
}

刪除

刪除操作較為復雜,我們先找到要刪除數的前驅和后繼,把前驅轉到根,后繼轉到根節點也就是前驅的子節點,此時顯然有后繼是前驅的右兒子,后繼的左兒子一定大於前驅小於后繼,也就是我們要刪除的數,根據前驅和后繼的定義,后繼一定有且僅有一個左兒子,對這個節點進行刪除即可

image

void erase(int x)
{
    int x_pre=find_pre_id(x),x_nxt=find_nxt_id(x);//找x的前驅后繼
    splay(x_pre,0);//把前驅轉到根
    splay(x_nxt,x_pre);//把后繼轉到根的子節點
    int cur=node[x_nxt].ch[0];//此時x一定是后繼的左兒子
    if(node[cur].cnt>1)//刪不完 
    {
        --node[cur].cnt;//減少一個 
        splay(cur,0);//轉 
    }
    else node[x_nxt].ch[0]=0;//切斷后繼的左子樹 
}

區間翻轉

能支持區間翻轉(即把區間內所有數換個位置,第一個與最后一個交換,第二個與倒數第二個交換……以此類推)是Splay Tree的一大特性,為了實現區間翻轉,Splay Tree上維護的就不再是權值,而改為維護下標

對於一次區間翻轉操作,我們利用剛剛提到的刪除操作的類似思想,把區間的前驅旋轉成為根,把區間的后繼旋轉成為根節點的右兒子,這時整個區間就成為了根的右兒子的左兒子的這棵子樹,我們給樹上的每一個節點都維護一個標記,表示它以及它的子樹是否被翻轉,每次一旦對於節點有操作,我們就把標記向下傳遞,同時交換自己的左右兒子並清除自身標記

PS:這里找區間的前驅后繼記得檢查你是否更改了自己的查詢 \(k\) 大數函數,這個函數的返回值應該改為樹上節點的下標而不是所維護的 \(v\)

在查詢最后結果時只需要中序遍歷整棵Splay Tree就可以獲得整棵樹操作后的編號順序

struct Node
{
    _Tp v;
    int cnt,siz,fa,ch[2];
    bool mark;//維護的翻轉標記
};
void push_down(int x)//下傳標記 
{
    if(node[x].mark)//如果有標記
    {
        //更新左右兒子標記(翻轉兩次就等於不翻轉,清除標記)
        node[node[x].ch[0]].mark^=1;
        node[node[x].ch[1]].mark^=1;
        node[x].mark=0;//清除自身標記
        swap(node[x].ch[0],node[x].ch[1]);//交換左右兒子
    }
}
void mid_find(int x)//中序遍歷 
{
    push_down(x);//遍歷時所有標記都要下傳
    if(node[x].ch[0]) mid_find(node[x].ch[0]);
    if(node[x].v>0&&node[x].v<=n) write(node[x].v,' ');
    if(node[x].ch[1]) mid_find(node[x].ch[1]);
}
int new_kth(int rank)//更改后的查詢第k大數
{
    ++rank;
    int cur=Root,son;
    if(node[cur].siz<rank) return INF;
    while(1)
    {
        push_down(cur);
        son=node[cur].ch[0];
        if(rank>node[son].siz+node[cur].cnt)
        {
            rank-=node[son].siz+node[cur].cnt;
            cur=node[cur].ch[1];
        }
        else if(node[son].siz>=rank) cur=son;
        else return cur;//在這里修改為返回下標!不要返回v
    }
}
void reverse(int l,int r)//區間翻轉(l到r)
{
	l=tre.kth(l-1);//找區間前驅后繼
	r=tre.kth(r+1);
	tre.splay(l,0);//把區間前驅后繼Splay上來
	tre.splay(r,l);
	tre.node[tre.node[tre.node[tre.Root].ch[1]].ch[0]].mark^=1;
    //根節點的右兒子的左兒子就是待翻轉的區間所在子樹的根,給它打上標記即可
}

初始化

在實際使用過程中,為了防止越界,我們常常會在樹中插入一個極小值,一個極大值,本文關於排名的代碼都是以已經插入極小值極大值為前提,自己編寫程序時一定要記得初始化插入一個極小值,一個極大值

純享版封裝Splay

這里新加入了動態開點功能,可以預先申請空間,也可以在使用時直接插入動態開點,非常方便

code

template<int N=10,typename _Tp=int,long long INF=2147483647> class Splay
{
	private:
		int Root,node_cnt;
		struct Node
		{
			_Tp v;
			int cnt,siz,fa,ch[2];
		};
		vector<Node> node;
		vector<int> del_list;
		int vir_alloc()//動態申請點 
		{
			if(!del_list.empty())
			{
				int tmp=del_list.back();
				del_list.pop_back();
				return tmp;
			}
			++node_cnt;
			if(node_cnt>=node.size())
			{
				if(node_cnt==node.capacity()) node.reserve(node.capacity()+15);
				node.emplace_back(Node());
			}
			return node_cnt;
		}
		void update(int x)//更新
		{
			node[x].siz=node[node[x].ch[0]].siz+node[node[x].ch[1]].siz+node[x].cnt;
		}
		void rotate(int x)//旋轉
		{
		    int y=node[x].fa,z=node[y].fa,k=(node[y].ch[1]==x);
		    node[z].ch[node[z].ch[1]==y]=x;
		    node[x].fa=z;
		    node[y].ch[k]=node[x].ch[k^1];
		    node[node[x].ch[k^1]].fa=y;
		    node[x].ch[k^1]=y;
		    node[y].fa=x;
		    update(y),update(x);
		}
		void splay(int x,int target)//轉到目標節點的兒子
		{
		    while(node[x].fa!=target)
		    {
		        int y=node[x].fa,z=node[y].fa;
		        if(z!=target)
		            ((node[z].ch[0]==y)^(node[y].ch[0]==x))?rotate(x):rotate(y);
		        rotate(x);
		    }
		    if(!target)Root=x;
		}
		void find(_Tp x)//對應值節點轉到根
		{
			int cur=Root;
			if(!cur)return;
			while(node[cur].ch[x>node[cur].v]&&x!=node[cur].v)
				cur=node[cur].ch[x>node[cur].v];
			splay(cur,0);
		}
	public:
		Splay()//初始化
		{
			Root=node_cnt=0;
			node.resize(N);
			node[0].siz=0,node[0].cnt=0,node[0].fa=0;
			insert(INF),insert(-INF);
		}
		void insert(_Tp x)//插入
		{
		    int cur=Root,from=0;
		    while(cur&&x!=node[cur].v)
		        from=cur,cur=node[cur].ch[x>node[cur].v];
		    if(cur)
		        ++node[cur].cnt;
		    else
		    {
				cur=vir_alloc();
		        if(!from) Root=cur;
		        else node[from].ch[x>node[from].v]=cur;
		        node[cur].v=x;
		        node[cur].cnt=1;
		        node[cur].fa=from;
		        node[cur].siz=1;
		        node[cur].ch[0]=node[cur].ch[1]=0;
		    }
		    splay(cur,0);
		}
		int find_pre_id(_Tp x)//查前驅編號 
		{
			find(x);
			if(node[Root].v<x)return Root;
			int cur=node[Root].ch[0];
			while(node[cur].ch[1]) cur=node[cur].ch[1];
			return cur;
		}
		int find_nxt_id(_Tp x)//查后繼編號 
		{
			find(x);
			if(node[Root].v>x)return Root;
			int cur=node[Root].ch[1];
			while(node[cur].ch[0]) cur=node[cur].ch[0];
			return cur;
		}
		_Tp find_pre(_Tp x)//查前驅值 
		{
			x=find_pre_id(x);
			return node[x].v;
		}
		_Tp find_nxt(_Tp x)//查后繼值 
		{
			x=find_nxt_id(x);
			return node[x].v;
		}
		void erase(_Tp x)//刪除 
		{
			int x_pre=find_pre_id(x),x_nxt=find_nxt_id(x);
			splay(x_pre,0);
			splay(x_nxt,x_pre);
			int cur=node[x_nxt].ch[0];
			if(node[cur].cnt>1)
			{
				--node[cur].cnt;
				splay(cur,0);
			}
			else del_list.emplace_back(node[x_nxt].ch[0]),node[x_nxt].ch[0]=0;
		}
		_Tp kth(int rank)//找排名為k的
		{
			++rank;
			int cur=Root,son;
			if(node[cur].siz<rank) return INF;
			while(1)
			{
				son=node[cur].ch[0];
				if(rank>node[son].siz+node[cur].cnt)
				{
					rank-=node[son].siz+node[cur].cnt;
					cur=node[cur].ch[1];
				}
				else if(node[son].siz>=rank) cur=son;
				else return node[cur].v;
			}
		}
		int get_rank(_Tp x)//查排名
		{
			find(x);
			if(node[Root].v>=x) return node[node[Root].ch[0]].siz;
			return node[node[Root].ch[0]].siz+node[Root].cnt;
		}
		void reserve(int cap)//直接申請更大空間 
		{
			if(node.capacity()<cap) node.reserve(cap);
		}
};

食用方法

將上方代碼加入您的代碼中,定義時您可以選擇性地提供三個可選參數,第一個可選參數 \(N\) 表示該Splay Tree的節點數預先申請空間,如果節點數超過 \(N\) 就會自動新申請空間,提供合適的大小能優化您的程序,該參數默認為 \(10\) 個節點

第二個可選參數 \(\_Tp\) 表示您在其中所存儲值的數據類型,第三個可選參數 \(INF\) 表示您所希望的正無窮大小,它的類型為long long,如果您不提供可選參數,\(\_Tp\) 將默認為int,\(INF\) 將默認為long long類型的 \(2147483647\)

Splay::reserve(int cap)函數能幫助您在使用過程中直接一次性申請更大的空間

具體見下方栗子

Splay<> A;
//定義一個名為A,初始申請節點空間10個,儲值類型為int,正無窮為2147483647的Splay Tree
Splay<19260817> B;
//定義一個名為B,初始申請節點空間19260817個,儲值類型為int,正無窮為2147483647的Splay Tree
Splay<500,long long> C;
//定義一個名為C,初始申請節點空間500個,儲值類型為long long,正無窮為2147483647的Splay Tree
Splay<114514,double,1919810> D;
//定義一個名為D,初始申請節點空間114514個,儲值類型為double,正無窮為1919810的Splay Tree
Splay<123,short int> E[233];
//定義一個名為E的容量為233的一維數組,每一個下標有一個初始申請節點空間123個,儲值類型為short int,正無窮為2147483647的Splay Tree 

//在(主)函數中
A.reserve(123456);//把名為A的Splay Tree的節點空間擴展到123456
int temp=654321;
A.reserve(temp);//把名為A的Splay Tree的節點空間擴展到654321

致謝

FJN 妹子 和 npy SYQ

OIWiki

博客園@Santiego


該文為本人原創,轉載請注明出處

博客園傳送門

洛谷傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM