1. 程式人生 > >關於Splay的學習感受

關於Splay的學習感受

                                       

【關於Splay】

之前記得五月份聽過一次外省金牌選手講過一次,然後七月份又講過一次,但本人腦子比較笨,當時完全聽得一臉懵逼啊,練了兩個月確實不一樣,現在談一下學習Splay的一些感受。

首先欲知Splay為何物,不得不先講講它的祖宗:二叉查詢樹,即BST(Binary Search Tree),關於二叉查詢樹,它不是一顆空樹就是擁有以下幾個性質的二叉樹:

1.樹中每個結點被賦予了一個權值;(下面假設不同結點的權值互不相同。)

2.若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

3.若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;

4.左、右子樹也分別為二叉查詢樹;

下圖就是一個合法的BST(蜜汁畫風【捂臉】):

通常二叉查詢樹支援如下操作:查詢一個元素x在集合中是否存在;插入或刪除一個元素,這些都可以通過遞迴實現。

但這也有個小問題,每個操作的複雜度都取決於樹的高度,那麼當這棵樹退化成一條鏈的時候,複雜度就退化成O(n)的了,由於二叉查詢樹的形態不一,所以平衡樹就出現了,它的每種操作最壞情況、均攤、期望的時間複雜度就是O(log n)的。而常用平衡樹有兩種:Splay和Treap,這裡介紹Splay,下一篇文章介紹Treap。

(廢話一堆然後轉入正題)

Splay,即伸展樹,是二叉查詢樹的一種改進,它與二叉查詢樹性質相似,與其不同的就是它可以自我調整。

關於Splay操作:伸展操作 Splay(x, S)是在保持伸展樹有序性的前提下,通過一系列旋轉將伸展樹 S 中的元素 x調整至樹的根部。在調整的過程中,要分以下三種情況分別處理:這個東西常數不能靠譜的可持久化,所以比起Treap而言,Splay的優勢似乎僅在於求LCT。

1.節點x的父節點y是根節點:如果x是左兒子,那麼就右旋一下,如果是右兒子,那麼就左旋一下,一次操作後,x就變成根節點了,完成,下面是示意圖:

2.節點x的父節點y不是根節點,y的父節點為z,且x,y均為父節點的左兒子或右兒子,那麼就右旋或左旋兩次,分別交換y,z和x,y,下面是示意圖(靈魂畫師再次上線):

3.節點x的父節點y不是根節點,y的父節點為z,x,y一個是左兒子,一個是右兒子,這時如果x是左兒子就先右旋再左旋,如果是右兒子就先左旋再右旋,注意兩次都是轉x,示意圖:

所以由上圖可以感性理解一下,經旋轉的平衡樹確實比之前要平衡很多,注意每次操作後都要進行伸展操作,然後這樣下來各操作複雜度均攤為O(log n)(因為不會證所以我就直接跳了)。

Splay支援以下操作:

1.Find(x,S):判斷元素x是否在伸展樹S表示的有序集中。首先,訪問根節點,如果x比根節點權值小則訪問左兒子;如果x比根節點權值大則訪問右兒子;如果權值相等,則說明x在樹中;如果訪問到空節點,則x不在樹中。如果x在樹中,則再執行Splay(x,S)調整伸展樹。

2.Insert(x,S):將元素x插入伸展樹S表示的有序集中。首先,訪問根節點,如果x比根節點權值小則訪問左兒子;如果x比根節點權值大則訪問右兒子;如果訪問到空節點t,則把x插入該節點,然後執行Splay(t,S)。

3.Merge(S1,S2):將兩個伸展樹S1與S2合併成為一個伸展樹。其中S1的所有元素都小於S2的所有元素。首先,我們找到伸展樹S1中最大的一個元素x,再通過Splay(x,S1)將x調整到伸展樹S1的根。然後再將S2作為x節點的右子樹。這樣,就得到了新的伸展樹S。如圖所示:

4.Delete(x,S):把節點x從伸展樹表示的有序集中刪除。首先,執行Splay(x,S)將x旋轉至根節點。然後取出x的左子樹S1和右子樹S2,執行Merge(S1,S2)把兩棵子樹合併成S。如圖所示:

5.Split(x,S):以x為界,將伸展樹S分離為兩棵伸展樹S1和S2,其中S1中所有元素都小於x,S2中的所有元素都大於x。首先執行Find(x,S),將元素x調整為伸展樹的根節點,則x的左子樹就是S1,而右子樹為S2。圖示同上。

除了以上操作,Splay還支援求最大值、最小值、前驅後繼,同時,Splay還能進行區間操作,對於待操作區間[l,r],我們將l-1通過Splay旋至根節點處,再將r+1旋至根節點右兒子處,這時r+1的左兒子就是待操作區間,可以像線段樹那樣打lazy-tag標記,然後具體實現見下方程式碼(主要給Rotate(左旋右旋合併版,只用打一段即可,不必打一個Zig打一個Zag),Splay,build,lazy,find(int k)(尋找第k大數),getnext(求後繼,前驅類似)):

(因為從來不打指標所以只能夠打打陣列)

結構體:

struct Num{                                //根據題目不同決定
	int val,id;
	bool operator<(const Num &a)const{
		if(val==a.val)
		  return id<a.id;
		return val<a.val;
	}
}So[MAXN/5];

struct Tr{                                 //平衡樹
	int fa,sum;
	int val,c[2],lz;
}tr[MAXN];
int tot,root,n;

1.Rotate:

void Rotate(int x,int k)
{
	if(tr[x].fa==-1)
	  return ;
	int fa=tr[x].fa,w;
	lazy(fa);
	lazy(x);
	tr[fa].c[!k]=tr[x].c[k];
	if(tr[x].c[k]!=-1)
	  tr[tr[x].c[k]].fa=fa;
	tr[x].fa=tr[fa].fa,tr[x].c[k]=fa;
	if(tr[fa].fa!=-1)
	{
		w=tr[tr[fa].fa].c[1]==fa;
		tr[tr[fa].fa].c[w]=x;
	}
	tr[fa].fa=x;
	Push(fa);
	Push(x);
}

2.Splay

void Splay(int x,int goal)
{
	if(x==-1)
	  return ;
	lazy(x);
	while(tr[x].fa!=goal)
	{
		int y=tr[x].fa;
		lazy(tr[y].fa);
		lazy(y),lazy(x);
		bool w=x==tr[y].c[1];
		if(tr[y].fa!=goal&&w==(y==tr[tr[y].fa].c[1]))
		  Rotate(y,!w);
		Rotate(x,!w);
	}
	if(goal==-1)
	  root=x;
	Push(x);
}

3.build

int build(int l,int r,int f)          //返回根節點
{
	if(r<l)
	  return -1;
	int mid=l+r>>1;
	int ro=newtr(mid,f,mid);
	data[mid]=ro;
	tr[ro].c[0]=build(l,mid-1,ro);
	tr[ro].c[1]=build(mid+1,r,ro);
	Push(ro);
	return ro;
}

4.lazy

void lazy(int id)                  //此懶標記表示交換左右兒子,即區間反轉
{
	if(tr[id].lz)
	{
		swap(lc,rc);
		tr[lc].lz^=1,tr[rc].lz^=1;
		tr[id].lz=0;
	}
}

5.find

int find(int k)
{
	int id=root;
	while(id!=-1)
	{
		lazy(id);
		int lsum=(lc==-1)?0:tr[lc].sum;
		if(lsum>=k)
		{
			id=lc;
		}
		else
		  if(lsum+1==k)
		    break;
		  else
		  {
		  	k=k-lsum-1;
		  	id=rc;
		  }
	}
	return id;
}

6.getnext

int Getnext(int id)
{
	lazy(id);
	int p=tr[id].c[1];
	if(p==-1)
	  return id;
	lazy(p);
	while(tr[p].c[0]!=-1)
	{
		p=tr[p].c[0];
		lazy(p);
	}
	return p;
}

關於Splay,在實現上還有許多細節,如每次操作後的Splay,此處不再贅述。

以上大概是我關於Splay的一些學習總結,以後可能還會填坑【PS:感謝wcr大佬的幫助】