伸展树 Splay

原理

众所周知,fhq treap 代码短小精悍,但是跑的慢。重中之重的是,LCT 与 Splay 有关。所以补一下 Splay 吧。其实 fhq treap 忘光了。

fhq treap

单旋

和 fhq treap 类似的,Splay 也是一棵二叉搜索树,有左小右大的性质。不过 fhq treap 核心操作为合并与分裂,Splay 的核心操作为伸展。

伸展即为树旋转,分为左旋 (ZAG) 和右旋 (ZIG)。如图,我们对根结点进行树旋转,可以发现左旋和右旋是对称的


以左旋为栗,其实是让绿(右儿子)成为红(root)的父亲,为了满足 AVL 的性质,红需要成为绿的左儿子。现在红没有右儿子(原来的右儿子绿被旋转上去了),恰巧绿的左儿子多了(绿成为了它的左儿子),那么把红多出来的左儿子蓝给绿当右儿子,仍然保持 AVL 的性质。

双旋

  • 同构双旋

  • 异构双旋

实现

辅助操作

  • 存储
struct node {
    
    
	int fa, ch[2] /*左 0 右 1*/, val, sz, cnt /*结点重复次数*/; 
}t[N];
int tot, root;
  • 查询父子关系
bool ident(int x, int f) {
    
    
	return t[f].ch[1] == x;
	//左 0 右 1 
}
  • 建立父子关系
void connect(int x, int f, int s /*左 0 右 1*/) {
    
    
	t[f].ch[s] = x, t[x].fa = f;
} 
  • 更新信息
void update(int now) {
    
    
	t[now].sz = t[t[now].ls].sz + t[t[now].rs].sz + t[now].cnt;
}
  • 新建结点
void newnode(int &now, int fa, int val) {
    
    
	now = ++tot;
	t[now].val = val, t[now].fa = fa;
	t[now].sz = t[now].cnt = 1; 
} 

伸展

  • rotate

传入一个结点 x x x,对它的父亲 f f f 进行旋转。rotate 操作可以看作将一个点向上调整。死记硬背容易混淆,建议现推。

void rotate(int x) {
    
    
	int f = t[x].fa, ff = t[f].fa, k = ident(x, f);
	connect(t[x].ch[k ^ 1], f, k); //挂 
	connect(x, ff, ident(f, ff)); //拎 
	connect(f, x, k ^ 1); //拎 
	update(f), update(x);
} 
  • 伸展

伸展操作依靠双旋来进行。在实现了 rotate 操作后,同构双旋需要先 rotate P P P,再 rotate X X X 。而异构双旋连续 rotate X X X 两次即可。找规律发现,如果 ident ⁡ ( X , P ) = ident ⁡ ( P , G ) \operatorname{ident}(X,P) = \operatorname{ident}(P,G) ident(X,P)=ident(P,G),那么是同构双旋,否则为异构双旋。

void splay(int x, int top) {
    
     //把 x 旋转到 top 的儿子,top 为 0 则旋转的根节点 
	if (!top) root = x;
	while (t[x].fa != top){
    
    
		int f = t[x].fa, ff = t[f].fa;
		if (ff != top) ident(x, f) ^ ident(f, ff) ? rotate(x) : rotate(f);
		rotate(x);
	}
}

基本操作

  • 插入结点

递归处理,插入结点后需要伸展到根节点。

void insert(int val, int &now = root, int fa = 0) {
    
    
	if (!now) newnode(now, fa, val), splay(now, 0);
	else if (val < t[now].val) insert(val, t[now].ch[0], now);
	else if (val > t[now].val) insert(val, t[now].ch[1], now);
	else t[now].cnt++, splay(now, 0);
}
  • 删除结点

先递归找到要删的点,将它伸展到根节点上。如果根节点的出现次数 > 1 >1 >1,那么减去 1 1 1 即可。否则要把根节点删掉。

扫描二维码关注公众号,回复: 11676797 查看本文章
  • 如果右子树为空,那么把 root 设为左儿子。
  • 如果右子树不为空,那么找到 root 的后继 x x x(在 root 右子树的左下),把他伸展到根节点的右儿子上。现在 x x x 的左子树为空,那么把 x x x 的左儿子设为 root 的左儿子。最后把 root 设为 x x x
void del(int val, int now = root) {
    
    
	if (val == t[now].val) {
    
    
		splay(x, 0);
		if (t[x].cnt > 1) t[x].cnt--;
		else if (t[x].ch[1]) {
    
    
			int p = t[x].ch[1];
			while (t[p].ch[0]) p = t[p].ch[0];
			splay(p, x);
			connect(t[x].ch[0], p, 0);
			root = p; update(p);
		} 
		else root = t[x].ch[0];
	} 
	else if (val < t[now].val) del(val, t[now].ch[0]);
	else del(val, t[now].ch[1]);
} 

猜你喜欢

转载自blog.csdn.net/qq_39984146/article/details/108119987