Splay詳解


平衡樹實際很簡單的

以下講解都以Luogu P3369 【模板】普通平衡樹為例

我不會帶指針的Splay,所以我就寫非指針型的Splay

Splay是基於二叉查找樹(bst)實現的

什么是二叉查找樹呢?就是一棵樹唄,但是這棵樹滿足性質:一個節點的左孩子一定比它小,右孩子一定比它大

比如:

1101696-20171125191307484-1881794250.png

這就是一棵最基本二叉查找樹

對於每次插入,它的期望復雜度大約是log^2 n級別的,但是存在極端情況,比如9999999 9999998 9999997.....1這種數據,會直接被卡成n^2級別

在這種情況下,平衡樹出現了!

1.定義Splay

struct node
{
    int v;//權值
    int fa;//父親節點
    int ch[2];//0代表左兒子,1代表右兒子
    int rec;//這個權值的節點出現的次數
    int sum;//子節點的數量
}tree[N];//N為節點最多有多少
int tot;//tot表示不算重復的有多少節點

2.Splay的核心

Rotate

首先考慮一下,我們要把一個點挪到根,那我們首先要知道怎么讓一個點挪到它的父節點

情況1:

當X是Y的左孩子時

1101696-20171125194733656-1618296206.png

ABC實際可以是子樹,但這里假設ABC都是點

這時候如果我們讓X成為Y的父親,只會影響到3組點的關系

B與X,X與Y,X與R

根據二叉排序樹的性質

B會成為Y的左兒子

Y會成為X的右兒子

X會成為R的兒子,具體是什么兒子,這個要看Y是R的啥兒子

1101696-20171125200053250-822796586.png

情況2:

當X是Y的右孩子

本質上是和情況1一樣的qaq

1101696-20171125200211625-2022743205.png

旋轉后變成

1101696-20171125200354937-579910866.png

能不能把這兩種情況合並呢qaq?結果是肯定的

我們需要一個函數來確定這個節點是他父節點的左孩子還是右孩子

inline bool findd(register int x)
{
	return tree[tree[x].fa].ch[0]==x?0:1;
}

如果是左孩子的話會返回0,右孩子會返回1

那么我們不難得到R,Y,X這三個節點的信息

int Y=tree[x].fa;
int R=tree[Y].fa;
int Yson=findd(x);
int Rson=findd(Y);

B的情況我們可以根據X的情況推算出來,根據^運算的性質,0^1=1,1^1=0,2^1=3,3^1=2,而且B相對於X的位置一定是與X相對於Y的位置是相反的

(否則在旋轉的過程中不會對B產生影響)

int B=tree[x].ch[Yson^1];

然后我們考慮連接的過程

根據上面的圖,不難得到(自己向上翻qaq)

1.B成為Y的哪個兒子與X是Y的哪個兒子是一樣的

2.Y成為X的哪個兒子與X是Y的哪個兒子相反

3.X成為R的哪個兒子與Y是R的哪個兒子相同

connect(B,Y,Yson);
connect(Y,x,Yson^1);
connect(x,R,Rson);

connect函數也很好寫

inline void connect(register int x,register int fa,register int son) //把x轉為fa的son(son是0/1,表示左孩子或右孩子)
{
	tree[x].fa=fa;
	tree[fa].ch[son]=x;
} 

Rotate代碼總覽(旋轉完不要忘了update):

inline void update(register int x)
{
	tree[x].sum=tree[tree[x].ch[0]].sum+tree[tree[x].ch[1]].sum+tree[x].rec;
}
inline bool findd(register int x)
{
	return tree[tree[x].fa].ch[0]==x?0:1;
}
inline void connect(register int x,register int fa,register int son) //把x轉為fa的son(son是0/1,表示左孩子或右孩子)
{
	tree[x].fa=fa;
	tree[fa].ch[son]=x;
} 
inline void rotate(register int x)
{
	int Y=tree[x].fa;
	int R=tree[Y].fa;
	int Yson=findd(x);
	int Rson=findd(Y);
	int B=tree[x].ch[Yson^1];
	connect(B,Y,Yson);
	connect(Y,x,Yson^1);
	connect(x,R,Rson);
	update(Y),update(x);
}

Splay

Splay(x,to)是實現把x節點搬到to節點

最簡單的辦法,對於x這個節點,每次上旋直到to

但是!

毒瘤的出題人可以構造數據把上面的這種方法卡到n^2 qaq*

下面我們介紹一下雙旋的Splay

這里的情況有很多,但是總的來說就三種情況

1.to是x的爸爸,

這樣的話吧x旋轉上去就好

if(tree[tree[x].fa].fa==to)
	rotate(x);

2.x和他爸爸和他爸爸的爸爸在一條線上(文字游戲)

其實就是findd(x)=findd(tree[x].fa)

這時候先把Y旋轉上去,再把X旋轉上去就好

if(findd(x)==find(tree[x].fa))
	rotate(tree[x].fa),rotate(x);

3.x和他爸爸和他爸爸的爸爸不在一條線上(和2相反)

這時候把X旋轉兩次就好

spaly函數的代碼:

inline void splay(register int x,register int to)
{
	to=tree[to].fa;
	while(tree[x].fa!=to)
	{
		int y=tree[x].fa;
		if(tree[y].fa==to)
			rotate(x);
		else if(findd(x)==findd(y))
			rotate(y),rotate(x);
		else
			rotate(x),rotate(x);
	}	
}

Splay的核心代碼到此結束

剩下的就是一些其他的東西(雖說有的也挺重要qaq)

3.其他的一些函數

insert

根據前面講的,我們在插入一個數之后,需要將其旋轉到根

首先,當這棵樹已經沒有節點的時候,我們直接新建一個節點就好

inline int newpoint(register int v,register int fa)
{
	tree[++tot].fa=fa;
	tree[tot].v=v;
	tree[tot].sum=tree[tot].rec=1;
	return tot; 
}

然后,當這可樹有節點的時候,我們根據二叉查找樹的性質,不斷向下走,直到找到一個可以插入的點,注意在走的時候需要更新一個每個節點的sum值

inline void Insert(register int x)
{
	int now=tree[0].ch[1];
	if(tree[0].ch[1]==0)
	{
		newpoint(x,0);
		tree[0].ch[1]=tot;
	}
	else
	{
		while(19260817)
		{
			++tree[now].sum;
			if(tree[now].v==x)
			{
				++tree[now].rec;
				splay(now,tree[0].ch[1]);
				return;
			}
			int nxt=x<tree[now].v?0:1;
			if(!tree[now].ch[nxt])
			{
				int p=newpoint(x,now);
				tree[now].ch[nxt]=p;
				splay(p,tree[0].ch[1]);
				return;
			}
			now=tree[now].ch[nxt];
		}
	}
}

delete

刪除的功能是:刪除權值為v的節點

我們不難想到:我們可以先找到他的位置,再把這個節點刪掉

找位置用find函數,不要和rotate的findd搞混

inline int find(register int v)
{
	int now=tree[0].ch[1];
	while(19260817)
	{
		if(tree[now].v==v)
		{
			splay(now,tree[0].ch[1]);
			return now;
		}
		int nxt=v<tree[now].v?0:1;
		if(!tree[now].ch[nxt])
			return 0;
		now=tree[now].ch[nxt];
	}
}

下面我們需要刪除函數

怎么樣才能保證刪除節點后整棵樹還滿足二叉查找樹的性質

此時會出現幾種情況

1.權值為v的節點已經出現過
這時候直接把他的rec和sum減去1就好
2.本節點沒有左右兒子
這樣的話就成了一棵空樹
3.本節點沒有左兒子
直接把他的右兒子設置成根
4.既有左兒子,又有右兒子
在它的左兒子中找到最大的,旋轉到根,把它的右兒子當做根(也就是它最大的左兒子)的右兒子

最后把這個節點刪掉就好

delete的代碼

inline void delet(register int x)
{
	int pos=find(x);
	if(!pos)
		return;
	if(tree[pos].rec>1)
	{
		--tree[pos].rec;
		--tree[pos].sum;
	}
	else
	{
		if(!tree[pos].ch[0]&&!tree[pos].ch[1])
			tree[0].ch[1]=0;
		else if(!tree[pos].ch[0])
		{
			tree[0].ch[1]=tree[pos].ch[1];
			tree[tree[0].ch[1]].fa=0;
		}
		else
		{
			int left=tree[pos].ch[0];
			while(tree[left].ch[1])
				left=tree[left].ch[1];
			splay(left,tree[pos].ch[0]);
			connect(tree[pos].ch[1],left,1);
			connect(left,0,1);
			update(left);
		}
	}
}

rank

1.查詢x數的排名

十分簡短

inline int rank(register int v)
{
	int pos=find(v);
	return tree[tree[pos].ch[0]].sum+1;
}

2.查詢排名為x的數

這個操作就是上面那個操作的逆向操作

inline int arank(register int x)
{
	int now=tree[0].ch[1];
	while(19260817)
	{
		int used=tree[now].sum-tree[tree[now].ch[1]].sum;
		if(x>tree[tree[now].ch[0]].sum&&x<=used)
		{
			splay(now,tree[0].ch[1]);
			return tree[now].v;
		}
		if(x<used)
			now=tree[now].ch[0];
		else
			x-=used,now=tree[now].ch[1];
	}
}

求前驅和后繼

前驅

這個更容易,我們可以維護一個ans變量,然后對整棵樹進行遍歷,同時更新ans

inline int lower(register int v)
{
	int now=tree[0].ch[1];
	int ans=-inf;
	while(now)
	{
		if(tree[now].v<v&&tree[now].v>ans)
			ans=tree[now].v;
		if(v>tree[now].v)
			now=tree[now].ch[1];
		else
			now=tree[now].ch[0];
	}
	return ans;
}

后繼

和前驅差不多

inline int upper(register int v)
{
	int now=tree[0].ch[1];
	int ans=inf;
	while(now)
	{
		if(tree[now].v>v&&tree[now].v<ans)
			ans=tree[now].v;
		if(v<tree[now].v)
			now=tree[now].ch[0];
		else
			now=tree[now].ch[1];
	}
	return ans;
}

4.Spaly整體代碼

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define N 100005
#define inf 1000000005
using namespace std;
inline int read()
{
	register int x=0,f=1;register char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
inline void write(register int x)
{
	if(!x)putchar('0');if(x<0)x=-x,putchar('-');
	static int sta[36];int tot=0;
	while(x)sta[tot++]=x%10,x/=10;
	while(tot)putchar(sta[--tot]+48);
}
struct node
{
    int v;
    int fa;
    int ch[2];
    int rec;
    int sum;
}tree[N];
int tot;
inline void update(register int x)
{
	tree[x].sum=tree[tree[x].ch[0]].sum+tree[tree[x].ch[1]].sum+tree[x].rec;
}
inline bool findd(register int x)
{
	return tree[tree[x].fa].ch[0]==x?0:1;
}
inline void connect(register int x,register int fa,register int son) //把x轉為fa的son(son是0/1,表示左孩子或右孩子)
{
	tree[x].fa=fa;
	tree[fa].ch[son]=x;
} 
inline void rotate(register int x)
{
	int Y=tree[x].fa;
	int R=tree[Y].fa;
	int Yson=findd(x);
	int Rson=findd(Y);
	int B=tree[x].ch[Yson^1];
	connect(B,Y,Yson);
	connect(Y,x,Yson^1);
	connect(x,R,Rson);
	update(Y),update(x);
}
inline void splay(register int x,register int to)
{
	to=tree[to].fa;
	while(tree[x].fa!=to)
	{
		int y=tree[x].fa;
		if(tree[y].fa==to)
			rotate(x);
		else if(findd(x)==findd(y))
			rotate(y),rotate(x);
		else
			rotate(x),rotate(x);
	}	
}
inline int newpoint(register int v,register int fa)
{
	tree[++tot].fa=fa;
	tree[tot].v=v;
	tree[tot].sum=tree[tot].rec=1;
	return tot; 
}
inline void Insert(register int x)
{
	int now=tree[0].ch[1];
	if(tree[0].ch[1]==0)
	{
		newpoint(x,0);
		tree[0].ch[1]=tot;
	}
	else
	{
		while(19260817)
		{
			++tree[now].sum;
			if(tree[now].v==x)
			{
				++tree[now].rec;
				splay(now,tree[0].ch[1]);
				return;
			}
			int nxt=x<tree[now].v?0:1;
			if(!tree[now].ch[nxt])
			{
				int p=newpoint(x,now);
				tree[now].ch[nxt]=p;
				splay(p,tree[0].ch[1]);
				return;
			}
			now=tree[now].ch[nxt];
		}
	}
}
inline int find(register int v)
{
	int now=tree[0].ch[1];
	while(19260817)
	{
		if(tree[now].v==v)
		{
			splay(now,tree[0].ch[1]);
			return now;
		}
		int nxt=v<tree[now].v?0:1;
		if(!tree[now].ch[nxt])
			return 0;
		now=tree[now].ch[nxt];
	}
}
inline void delet(register int x)
{
	int pos=find(x);
	if(!pos)
		return;
	if(tree[pos].rec>1)
	{
		--tree[pos].rec;
		--tree[pos].sum;
	}
	else
	{
		if(!tree[pos].ch[0]&&!tree[pos].ch[1])
			tree[0].ch[1]=0;
		else if(!tree[pos].ch[0])
		{
			tree[0].ch[1]=tree[pos].ch[1];
			tree[tree[0].ch[1]].fa=0;
		}
		else
		{
			int left=tree[pos].ch[0];
			while(tree[left].ch[1])
				left=tree[left].ch[1];
			splay(left,tree[pos].ch[0]);
			connect(tree[pos].ch[1],left,1);
			connect(left,0,1);
			update(left);
		}
	}
}
inline int rank(register int v)
{
	int pos=find(v);
	return tree[tree[pos].ch[0]].sum+1;
}
inline int arank(register int x)
{
	int now=tree[0].ch[1];
	while(19260817)
	{
		int used=tree[now].sum-tree[tree[now].ch[1]].sum;
		if(x>tree[tree[now].ch[0]].sum&&x<=used)
		{
			splay(now,tree[0].ch[1]);
			return tree[now].v;
		}
		if(x<used)
			now=tree[now].ch[0];
		else
			x-=used,now=tree[now].ch[1];
	}
}
inline int lower(register int v)
{
	int now=tree[0].ch[1];
	int ans=-inf;
	while(now)
	{
		if(tree[now].v<v&&tree[now].v>ans)
			ans=tree[now].v;
		if(v>tree[now].v)
			now=tree[now].ch[1];
		else
			now=tree[now].ch[0];
	}
	return ans;
}
inline int upper(register int v)
{
	int now=tree[0].ch[1];
	int ans=inf;
	while(now)
	{
		if(tree[now].v>v&&tree[now].v<ans)
			ans=tree[now].v;
		if(v<tree[now].v)
			now=tree[now].ch[0];
		else
			now=tree[now].ch[1];
	}
	return ans;
}
int main()
{
	int m=read();
	while(m--)
	{
		int opt=read(),x=read();
		if(opt==1)
			Insert(x);
		else if(opt==2)
			delet(x);
		else if(opt==3)
		{
			write(rank(x));
			printf("\n");
		}
		else if(opt==4)
		{
			write(arank(x));
			printf("\n");
		}
		else if(opt==5)
		{
			write(lower(x));
			printf("\n");
		}
		else
		{
			write(upper(x));
			printf("\n");
		}
	}
	return 0;
}

5.相關題目

1.Luogu P2234 [HNOI2002]營業額統計

平衡樹板題

2.Luogu P1503 鬼子進村

平衡樹板題

3.Luogu P3871 [TJOI2010]中位數

平衡樹板題

4.Luogu P1533 可憐的狗狗

莫隊+平衡樹苟過

5.Luogu P2073 送花

平衡樹板題

以上是splay的基本應用qaq

還有一種操作沒講,就是如何進行區間操作

實現起來很簡單

假設我們要在[l,r]之間上搞事情,我們首先把l的前驅旋轉到根節點,再把r的后繼轉到根節點的右兒子

那么此時根節點右兒子的左兒子代表的就是區間[l,r]

這應該很好理解qaq

然后就可以像線段樹的lazy標記一樣,給區間l,rl,r打上標記,延遲更新,比如區間反轉的時候更新的時候直接交換左右兒子

我們下面以P3391 【模板】文藝平衡樹(Splay)為例

這里有一個巧:如果一個區間被打了兩次,那么就相當於不打

所以我們用一個bool變量來儲存該節點是否需要被旋轉

pushdown函數可以這么寫(rev就是翻轉標記)

inline void pushdown(register int x)
{
    if(tree[x].rev)
    {
        swap(tree[x].ch[0],tree[x].ch[1]);
        tree[tree[x].ch[0]].rev^=1;
        tree[tree[x].ch[1]].rev^=1;    
        tree[x].rev=0;
    }
}

這道題完整代碼

#include <bits/stdc++.h>
#define N 100005
#define inf 0x7fffff
using namespace std;
inline int read()
{
	register int x=0,f=1;register char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
inline void write(register int x)
{
	if(!x)putchar('0');if(x<0)x=-x,putchar('-');
	static int sta[36];int cnt=0;
	while(x)sta[cnt++]=x%10,x/=10;
	while(cnt)putchar(sta[--cnt]+48);
}
int n,m;
struct node{
	int fa,ch[2],tot;
	bool rev;
}tree[N];
int root,PosL,PosR;
inline bool findd(register int x)
{
	return x==tree[tree[x].fa].ch[1];
}
inline void connect(register int x,register int fa,register int son)
{
	tree[x].fa=fa;
	tree[fa].ch[son]=x;
}
inline void update(register int x)
{
	tree[x].tot=tree[tree[x].ch[0]].tot+tree[tree[x].ch[1]].tot+1;
}
inline void rotate(register int x)
{
	int Y=tree[x].fa;
	if(Y==root)
		root=x;
	int R=tree[Y].fa;
	int Yson=findd(x);
	int Rson=findd(Y);
	int B=tree[x].ch[Yson^1];
	connect(B,Y,Yson);
	connect(Y,x,Yson^1);
	connect(x,R,Rson);
	update(Y);
	update(x);
}
inline void splay(register int x,register int to)
{
	while(tree[x].fa!=to)
	{
		int y=tree[x].fa;
		if(tree[y].fa==to)
			rotate(x);
		else if(findd(x)==findd(y))
			rotate(y),rotate(x);
		else
			rotate(x),rotate(x);
	}
	update(x);
}
inline int buildsplay(register int l,register int r)
{
	if(l>r)
		return 0;
	int mid=l+r>>1;
	connect(buildsplay(l,mid-1),mid,0);
	connect(buildsplay(mid+1,r),mid,1);
	tree[mid].rev=0;
	update(mid);
	return mid;
}
inline void pushdown(register int x)
{
	if(tree[x].rev)
	{
		swap(tree[x].ch[0],tree[x].ch[1]);
		tree[tree[x].ch[0]].rev^=1;
		tree[tree[x].ch[1]].rev^=1;
		tree[x].rev=0;
	}
}
inline int find(register int x)
{
	int now=root;
	--x;
	pushdown(now);
	while(x!=tree[tree[now].ch[0]].tot)
	{
		if(tree[tree[now].ch[0]].tot<x)
			x-=tree[tree[now].ch[0]].tot+1,now=tree[now].ch[1];
		else
			now=tree[now].ch[0];
		pushdown(now);
	}
	return now;
}
inline void print(register int now)
{
	if(!now)
		return;
	pushdown(now);
	print(tree[now].ch[0]);
	if(now!=1&&now!=n+2)
		write(now-1),putchar(' ');
	print(tree[now].ch[1]);
}
int main()
{
	n=read(),m=read();
	root=buildsplay(1,n+2);
	while(m--)
	{
		int l=read(),r=read();
		PosL=find(l);
		splay(PosL,0);
		PosR=find(r+2);
		splay(PosR,root);
		tree[tree[PosR].ch[0]].rev^=1;
	}
	print(root);
	return 0;
 } 


免責聲明!

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



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