hihocoder #1329 : 平衡树·Splay

#1329 : 平衡树·Splay

时间限制: 10000ms
单点时限: 1000ms
内存限制: 256MB
描述

小Ho:小Hi,上一次你跟我讲了Treap,我也实现了。但是我遇到了一个关键的问题。

小Hi:怎么了?

小Ho:小Hi你也知道,我平时运气不太好。所以这也反映到了我写的Treap上。

小Hi:你是说你随机出来的权值不太好,从而导致结果很差么?

小Ho:就是这样,明明一样的代码,我的Treap运行结果总是不如别人。小Hi,有没有那种没有随机因素的平衡树呢?

小Hi:当然有了,这次我就跟你讲讲一种叫做Splay的树吧。而且Splay树能做到的功能比Treap要更强大哦。

小Ho:那太好了,你快告诉我吧!

输入

第1行:1个正整数n,表示操作数量,100≤n≤200,000

第2..n+1行:可能包含下面3种规则:

1个字母'I',紧接着1个数字k,表示插入一个数字k到树中,1≤k≤1,000,000,000,保证每个k都不相同

1个字母'Q',紧接着1个数字k。表示询问树中不超过k的最大数字

1个字母'D',紧接着2个数字a,b,表示删除树中在区间[a,b]的数。

输出

若干行:每行1个整数,表示针对询问的回答,保证一定有合法的解

样例输入
6
I 1
I 2
I 3
Q 4
D 2 2
Q 2
样例输出
3

1


第一次领略到splay tree的厉害。。

小Hi:Splay树,中文名一般叫做伸展树。

和Treap树相同,作为平衡树,它也是通过左旋和右旋来调整树的结构。

这里我们再复习一下左旋和右旋操作:

若以x作为参数(注意上一讲中是以p作为参数),其对应的伪代码分别为:

right-rotate(x):
	p = x.father
	x.father = p.father
	If (p.father is not empty) Then
		If (p.father.left == p) Then
			p.father.left = x
		Else
			p.father.right = x
		End If
	Else
		root = x
	End If
	p.left = x.right
	x.right.father = p
	x.right = p
	p.father = x
	
left-rotate(x):
	p = x.father
	x.father = p.father
	If (p.father is not empty) Then
		If (p.father.left == p) Then
			p.father.left = x
		Else
			p.father.right = x
		End If
	Else
		root = x
	End If
	p.right = x.left
	x.left.father = p
	x.left = p
	p.father = x

和Treap树不同的是,Splay树不再用一个随机的权值来进行平衡,而是用固定的调整方式来使得调整之后的树会比较平衡。

在左旋右旋的基础上,Splay树定义了3个操作:

1. Zig

直接根据x节点的位置,进行左旋或右旋。

该操作将x节点提升了一层。


2. Zig-Zig

若p不是根节点,还有父亲节点g,且p和x同为左儿子或右儿子,则进行Zig-Zig操作:

当x,p同为左儿子时,依次将p和x右旋;

当x,p同为右儿子时,依次将p和x左旋。

注意此处不是将x连续Zig两次。该操作将x节点提升了两层。


3. Zig-Zag

若p不是根节点,则p还有父亲节点g。且p和x同为左儿子或右儿子,则进行Zig-Zag操作:

当p为左儿子,x为右儿子时,将x节点先左旋再右旋;

当p为右儿子,x为左儿子时,将x节点先右旋再左旋。

该操作将x节点提升了两层。


进一步在Zig,Zig-Zig和Zig-Zag操作上,Splay树定义了"Splay"操作。

对于x以及x的祖先y,splay(x, y),表示对x节点进行调整,使得x是y的儿子节点:

splay(x, y):
	While (x.father != y)
		p = x.father
		If (p.father == y) Then
			// 因为p的父亲是y,所以只需要将x进行Zig操作
			// 就可以使得x的父亲变为y
			If (p.left == x) Then
				right-rotate(x)
			Else
				left-rotate(x)
			End If
		Else
			g = p.father
			If (g.left == p) Then
				If (p.left == x) Then
					// x,p同为左儿子,Zig-Zig操作
					right-rotate(p)
					right-rotate(x)
				Else
					// p为左,x为右,Zig-Zag操作
					left-rotate(x)
					right-rotate(x)
				End If
			Else
				If (p.right == x) Then
					// x,p同为右儿子,Zig-Zig操作
					left-rotate(p)
					left-rotate(x)
				Else 
					// p为右,x为左,Zig-Zag操作
					right-rotate(x)
					left-rotate(x)
				End If
			End If
		End If
	End While

在执行这个操作的时候,需要保证y节点一定是x节点祖先。

值得一提的是,大多数情况下我们希望通过splay操作将x旋转至整棵树的根节点。此时只需令y=NULL即可实现。


小Ho:旋转和Splay我懂了,但是要怎么运用上去呢?

小Hi:Splay树的插入和查询操作和普通的二叉搜索树没有什么大的区别,需要注意的是每次插入和查询结束后,需要对访问节点做一次Splay操作,将其旋转至根。

insert(key):
	node = bst_insert(key) // 同普通的BST插入, node为当前插入的新节点
	splay(node, NULL)
	
find(key):
	node = bst_find(key) // 同普通的BST查找, node为查找到的节点
	splay(node, NULL)

同时由于Splay的特性,我们还有两个特殊的查询操作。在树中查找指定数key的前一个数和后一个数。

我们先将key旋转至根,那么key的前一个数一定是根节点左儿子的最右子孙,同时key的后一个数一定是根节点右儿子的最左子孙。

findPrev(key):
	splay( find(key), NULL )
	node = root.left
	While (node.right)
		node = node.right
	Return node
	
	findNext(key):
	splay( find(key), NULL )
	node = root.right
	While (node.left)
		node = node.left
	Return node

splay中的删除key操作:

splay的删除可以采用和一般二叉搜索树相同的方法:即先找到节点key,若key没有儿子则直接删去;若key有1个儿子,则用儿子替换掉x;若key有2个儿子,则通过找到其前(或后)一个节点来替换掉它,最后将该节点Splay到根。

同时,这里还有另一种方法来完成删除操作:

首先我们查找到key的前一个数prev和后一个数next。将prev旋转至根,再将next旋转为prev的儿子。

此时key节点一定是next的左儿子。那么直接将next的左儿子节点删去即可。

delete(key):
	prev = findPrev(key)
	next = findNext(key)
	splay(prev, NULL)
	splay(next, prev)
	next.left = NULL

这里你可能会担心如果key是数中最小或者是最大的数怎么办?

一个简单的处理方式是手动加入一个超级大和超级小的值作为头尾。


那么小Ho,这里有一个问题,假如要删除一个区间[a,b]的数该怎么做?

小Ho:我想想...我知道了!

因为要删除[a,b],那么我就要想办法把[a,b]的数旋转到一个子树上,再将这个子树删掉就行了。

方法和删除一个数相同,我首先将a的前一个数prev和b的后一个数next找出来。

同样将prev旋转至根,再将next旋转为prev的儿子。

那么此时next的左子树一定就是所有[a,b]之间的数了!

deleteInterval(a, b):
	prev = findPrev(a)
	next = findNext(b)
	splay(prev, NULL)
	splay(next, prev)
	next.left = NULL

小Hi:没错,那么下一个问题!如果a,b不在树中呢?

小Ho:这还不简单,把a,b插入树中,做完之后再删除不就好了!

小Hi:想不到小Ho你还蛮机智的嘛。

小Ho:那是,毕竟是我小Ho。(哼哼)

小Hi:Splay树由于splay操作的使得其相较于Treap具有更大的灵活性,并且不再有随机性。其插入、查找和删除操作的均摊时间复杂度也都是O(logn)的,具体的复杂度分析可以参考这里。那么最后小Ho你能够把Splay的实现出来么?

小Ho:没问题,看我的吧!


AC:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int inf = 0x3f3f3f3f;
struct Node {
	int val;
	struct Node *father;
	struct Node *left,*right;

	Node(){}
	Node(int key) {
		this->val = key;
		this->father = this->left = this->right = NULL;
	}
};
Node *root;

void left_rotate(Node *x) {
	Node *p = x->father;
	x->father = p->father;
	if(p->father != NULL) { //左旋
		if(p->father->left == p) {
			p->father->left = x;
		} else if(p->father->right == p) {
			p->father->right = x;
		}
	} else {  //说明此时的p是根节点
		root = x;
	}
	p->right = x->left;
	if(x->left != NULL)
		x->left->father = p;
	x->left = p;
	p->father = x;
}

void right_rotate(Node *x) {
	Node *p = x->father;
	x->father = p->father;
	if(p->father != NULL) { //右旋
		if(p->father->left == p) {
			p->father->left = x;
		} else if(p->father->right == p) {
			p->father->right = x;
		}
	} else {  //说明此时的p是根节点
		root = x;
	}
	p->left = x->right;
	if(x->right != NULL)
		x->right->father = p;
	x->right = p;
	p->father = x;
}

void Splay(Node *x,Node *y) { //表示对x节点进行调整,使得x是y的儿子节点
	while(x->father != y) {
		Node *p = x->father;
		if(p->father == y) {
			// 因为p的父亲是y,所以只需要将x进行Zig操作
			// 就可以使得x的父亲变为y
			if(p->left == x) {
				right_rotate(x);
			} else {
				left_rotate(x);
			}
		} else {
			Node *g = p->father;
			if(p == g->left) {
				if(x == p->left) {  
					// x,p同为左儿子,Zig-Zig操作
					//当x,p同为左儿子时,依次将p和x右旋
					//当x,p同为右儿子时,依次将p和x左旋
					right_rotate(p);
					right_rotate(x);
				} else { 
					// p为左,x为右,Zig-Zag操作
					//当p为左儿子,x为右儿子时,将x节点先左旋再右旋
					//当p为右儿子,x为左儿子时,将x节点先右旋再左旋
					left_rotate(x);
					right_rotate(x);
				}
			} else {
				if(x == p->right) {
					// x,p同为右儿子,Zig-Zig操作
					left_rotate(p);
					left_rotate(x);
				} else {
					// p为右,x为左,Zig-Zag操作
					right_rotate(x);
					left_rotate(x);
				}
			}
		}
	}
}

Node *bst_insert(int key) { //类似平衡二叉树先插入节点
	Node *p = new Node(key);
	if(root == NULL) {
		root = p;
		return root;
	}
	Node *t = root;
	while(true) {
		if(key > t->val) { //右枝走
			if(t->right == NULL) {
				t->right = p;
				p->father = t;
				break;
			}
			t = t->right;
		} else if(key < t->val) {  //左枝走
			if(t->left == NULL) {
				t->left = p;
				p->father = t;
				break;
			}
			t = t->left;
		} else return t; //说明有重复元素
	}
	return p;
}

Node *bst_find(int key) {  //普通的平衡二叉树的查找
	Node *t = root;
	while(t->val != key) {
		if(key > t->val) {
			if(t->right != NULL) { 
				t = t->right;
			} else return NULL; 
		} else {
			if(t->left != NULL) {
				t = t->left;
			} else return NULL;
		}
	}
	return t;
}

void insert(int key) {
	Node *p = bst_insert(key);
	Splay(p,NULL);
}

Node *find(int key) {
	Node *p = bst_find(key);
	if(p != NULL) {
		Splay(p,NULL);
	}
	return p;
}

Node *findPre(int key) {  //查找指定数key的前一个数
	Node *p = find(key);
	if(p == NULL) return NULL;
	Splay(p,NULL);
	Node *t = root->left;
	if(t == NULL) return root;
	while(t->right) {
		t = t->right;
	}
	return t;
}

Node *findNext(int key) {
	Node *p = find(key);
	if(p == NULL) return NULL;
	Splay(p,NULL);
	Node *t = root->right;
	if(t == NULL) return root;
	while(t->left) {
		t = t->left;
	}
	return t;
}

void Delete(int key) {
	Node *pre = findPre(key);
	Node *next = findNext(key);
	if(pre != NULL && next != NULL) {
		Splay(pre,NULL);
		Splay(next,pre);
		next->left = NULL;
	}
}

void deleteInterval(int a,int b) {
	insert(a);
	insert(b);
	Node *pre = findPre(a);
	Node *next = findNext(b);
	Splay(pre,NULL);
	Splay(next,pre);
	next->left = NULL;
	Delete(a);
	Delete(b);
}

int main()
{
	int n,k,a,b;
	char op[2];
	while(scanf("%d",&n)!=EOF) {
		root = NULL;
		insert(-1);
		insert(inf);
		while(n--) {
			scanf("%s",op);
			if(op[0] == 'I') {
				scanf("%d",&k);
				insert(k);
			} else if(op[0] == 'Q') {
				scanf("%d",&k);
				Node *p = find(k);
				if(p != NULL) {
					printf("%d\n",k);
				} else {
					insert(k);
					p = findPre(k);
					Delete(k);
					printf("%d\n",p->val);
				}
			} else {
				scanf("%d %d",&a,&b);
				deleteInterval(a,b);
			}
		}
	}
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值