Splay 超详细解释 + 模板

备注 : 区间翻转 暂缺

前言

上个月开始打Splay然后被老师看见拉去做USACO题库......

然后这个月继续 对着标的思路打 打完放上去......92分??

对着狂改多次 然后放上去......92分?

干脆直接把标放上去......92分?

真的是气急败♂坏了

然后换了标......重复上续步骤......92分?

抓狂23333~

重复多次步骤 然后跑了57遍 OK finished 过了 心力交瘁 少说码了 3W byte了

然而打的过程早会了的说...... (那你怎么只有92??)

好了不扯那么多 现在进入

正题

先翻翻概念了解一下 自行百度 或 戳这里

Splay是什么呢 嗯 就是能让节点比较均匀地分布在树上的二叉搜索树

又慢 代码量又长 如果不是LCT要用到我才不学

好啦 我在这里再略讲一下操作

先来看看核心的操作

1.旋转 (Rotate)

请先打开给的概念的链接 (就是上面的那个戳这里) 乱翻一通 看看两种旋转

首先要知道为什么要旋转 想知道? 既然你诚心诚意地问了 我就大发慈悲地告诉你 为了防止世界被破坏 为了守

我们得到的树呀 不一定是随机数据的说 如果遇到毒瘤出题人 给你几条长为1e5的链

那该怎么办? 因此我们要在任何时候都旋转一下 (不然刚好让你旋转成类似链的树时 全是查询怎么办)

旋转只有一种 那就是上旋 旋转只有两种 那就是左旋和右旋

但是旋转要根据当前点的状态 (是其父亲的哪个儿子) 和 当前点父亲的状态 (有无父亲 是其父亲的哪个儿子) 来判断

举个栗子 如果当前点是他父亲的左儿子 然后你让他左旋 不就断了么 =-= 这是离间他们的亲子关系!(给个愤怒的表情自己体会)

因此要根据其父和其祖父任意两代之间的相对位置判断左右旋 栗子 如果其父是其祖父的左儿子 右旋其父

关于旋转带来的点的元素改变......

首先考虑先改变哪个点的关系 这里先定义当前点 x 及其父亲 y 还有其祖父 z 以及方便左右旋设置的 mode (0为右旋 1为左旋)

祖父的点肯定是不变的啦 但 x 和 y 是会变的 因此 z 的 父子关系会变更 先改这个......哒哒哒 改完啦

然后就是 x 和 y 的 除子节点外的其他改变了的信息了 包括儿子从属 相对父子关系 这里要先判断一下是左旋还是右旋 更新一下 mode Tip:更新儿子从属关系可用异或 (想换成 if 也不是不行) 然后信息更改顺序可以随心换 但别漏了 =-=

最后更新子节点数量 为什么呢? 因为 x 的 儿子 和 y 的 另一个儿子 是 不变的 然后更新完 x 和 y 后 就可以用两点的新的儿子轻易地改变其子节点数量 这里记得先更新 y 的 因为 y 更新后 变成了 x 的 儿子 但是 其子节点数量还没变 最后更新 x 的 子节点数量

下放代码片段

void rotate(int x)
{
	int y = e[x].fa, z = e[y].fa,mode = 0;
	if (e[z].son[0] == y) e[z].son[0] = x;
	else e[z].son[1] = x;
	e[x].fa = z;
	//更新 z 的 有关元素
	if (e[y].son[0] == x) ++mode;
	e[y].son[mode ^ 1] = e[x].son[mode];
	e[e[x].son[mode]].fa = y;
	e[x].son[mode] = y;
	e[y].fa = x;
	//更新 x 和 y 的 关系 (互换)
	e[y].siz = e[e[y].son[0]].siz + e[e[y].son[1]].siz + e[y].tie;
	e[x].siz = e[e[x].son[0]].siz + e[e[x].son[1]].siz + e[x].tie;
	//最后更新子节点数量
}

2.展开 (Splay)

我去手贱退了界面又要重打 =-= 这部分是带火气打的

其实这个子程序就是 让一个点旋转到根 没旋转到继续旋 然后用的不是深搜 而是一个 while (当前点还有父亲)

然后记一下当前点 x 的 父亲 和 祖父 分别为 y 和 z

首先 如果当前点没有祖父 就只用旋转一次 就能到根 直接 rotate 他自己 Tip: 判断左旋右旋 在 rotate 子程序里

如果有祖父的话 就有四种情况了 这个时候 就先翻回上面的那个戳这里 然后乱翻一气吧

吶 看见没 如果相邻两代的从属关系是一样的 (都是左儿子 或 都是右儿子) 就要先旋转当前点父亲 再旋转他自己

如果从属关系不一样 就直接旋转他自己 那个网站里讲的比较复杂 我翻了其他 dalao 的 程序

居然可以用 异或! 吶 判断两从属关系一样时 == 就得 1 然后 1 ^ 1 = 0 或者 0 ^ 0 = 0 的时候 旋转其父

否则得到 1 就旋转他自己 妙极!

当然如果这样的话 mode 的赋值要丢到 rotate 里去了 放外面我居然 TLE 8 个点 还错了一个

最后别忘了树根 (root) 也要更新哈 改成当前点

一次 splay 后 最坏情况 (链) 当前点插的地方的深度 居然可以减少一半!

所以 有事没事 都要 splay 一下 这是重点!

因此 有时 splay 是为了方便处理问题 有时却是为了调整平衡

然后下放代码片段

void splay(int x)
{
	while (e[x].fa)
	{
		int y = e[x].fa, z = e[y].fa;
		if (z == 0) rotate(x); else {
			if ((e[z].son[0] == y) ^ (e[y].son[0] == x)) rotate(x);
			else rotate(y);
			rotate(x);
		}
	}
	root = x;
}

3.删除 (del)

我英语好 我知道是 delete =-= 为了主程序漂亮 所以改了下

这个理解比较麻烦 也分四种情况

1.删除的值不存在

    直接退掉啦

2.删除的值大于一个

    直接删掉一个出现次数就好啦

如果删除的值只有一个就麻烦了 Tip: 下面两种最后都要清空原根的各个元素 新根的父亲变为0

3.删除的值只有一个 且 没有左儿子

    吼吼 这个的话直接把当前根的右儿子变成根就好啦

4.删除的值只有一个 且 有左儿子

    这个我们要先切断左儿子和根的关系 (只用先改左儿子的父亲元素 然后其父亲的修改可以和三一起放判断外面改)

    然后呢 在左儿子处开始推 找到原根的左子树里最大的值

    之后 把该左子树里最大的值旋转上来 并把他当成根

    因为他没右儿子 (他是原根的左字数里最大的) 于是就可以把原根的右儿子接到新根上 更新关系

    然后清空原根的各个元素 就行了

下放代码片段

void del(int p)
{
	int pos = find(root, p);
	if (e[pos].v != p) return;
	splay(pos);
	if (e[pos].tie > 1) {
		--e[pos].tie;
		--e[pos].siz;
		return;
	}
	if (!e[pos].son[0]) {
		e[e[pos].son[1]].fa = 0;
		root = e[pos].son[1];
		if (!root) tot = 0;
	} else {
		e[e[pos].son[0]].fa = 0;
		int lax = find(e[pos].son[0],MAX);
		splay(lax);
		e[root].siz += e[e[pos].son[1]].siz;
		e[root].son[1] = e[pos].son[1];
		e[e[pos].son[1]].fa = root;
	}
	e[pos].v = 0;
	e[pos].tie = 0;
	e[pos].siz = 0;
	e[pos].fa = 0;
	e[pos].son[0] = 0;
	e[pos].son[1] = 0;
}

好啦 其他的操作就在代码里随便标一下吧 想认真了解的话 就翻回上面的那个戳这里

下放 认真缩减但没怎么压行 的 代码 然后 例题仍旧是洛谷的模板

#include <cstdio>
#define MAX 0x7fffffff //2147483647
#define MIN 0x80000000 //-2147483648
using namespace std;
const int MAXN = 100010;
struct Node_attribute {
	int fa,son[2],v,tie,siz;
} e[MAXN];
int root,tot;
void rotate(int x) //见上详解 懒得打了
{
	int y = e[x].fa, z = e[y].fa,mode = 0;
	if (e[z].son[0] == y) e[z].son[0] = x;
	else e[z].son[1] = x;
	e[x].fa = z;
	if (e[y].son[0] == x) ++mode;
	e[y].son[mode ^ 1] = e[x].son[mode];
	e[e[x].son[mode]].fa = y;
	e[x].son[mode] = y;
	e[y].fa = x;
	e[y].siz = e[e[y].son[0]].siz + e[e[y].son[1]].siz + e[y].tie;
	e[x].siz = e[e[x].son[0]].siz + e[e[x].son[1]].siz + e[x].tie;
}
void splay(int x) //见上详解 懒得打了
{
	while (e[x].fa)
	{
		int y = e[x].fa, z = e[y].fa;
		if (!z) rotate(x); else {
			if ((e[z].son[0] == y) ^ (e[y].son[0] == x)) rotate(x);
			else rotate(y);
			rotate(x);
		}
	}
	root = x;
}
int find(int now,int w) //查找函数是基于二叉搜索树的有序性(节点值按中序遍历从小到大)
{ //然后这个如果查找不到 就会返回与他相差最小的点
	while (e[now].v != w)
		if (w < e[now].v) { //比较值
			if (e[now].son[0]) now = e[now].son[0]; //值小则往左儿子走
			else break; //找不到 退出
		} else {
			if (e[now].son[1]) now = e[now].son[1]; //值大则往右儿子走
			else break; //找不到 退出
		}
	return now; //返回数值
}
void add(int f,int w)
{ //为了方便添加节点弄的子程序 把两种情况合在一起 可以不用 =-=
	e[++tot].fa = f;
	e[tot].tie = 1;
	e[tot].siz = 1;
	e[tot].v = w;
}
void ins(int p)
{
	if (!tot) { //空树
		add(0,p);
		root = 1;
		return;
	}
	int pos = find(root,p); //寻找点的位置 如果不存在 则为匹配加入点的父亲
	if (e[pos].v == p) e[pos].tie++; else { //如果当前值存在 其出现次数 + 1 否则...
		add(pos,p); //新增节点
		if (p < e[pos].v) e[pos].son[0] = tot; //家庭分配儿子承包责任制
		else e[pos].son[1] = tot;
	} //这两句并不会覆盖原有的点哦 因为..原本这地方不会有点呀 或者只有一个 但不是他的位置
	for (int now = pos ; now ; ++e[now].siz,now = e[now].fa); //当前点及其所有祖先的子节点个数要 + 1
	if (pos) splay(pos); //如果点存在 旋转他到根
	else splay(tot); //否则新加的点旋转到根
}
void del(int p)
{
	int pos = find(root, p); //找要删除的点的位置
	if (e[pos].v != p) return; //如果点不存在还删什么..直接退掉
	splay(pos); //旋转到根方便最后两种情况处理
	if (e[pos].tie > 1) { //当前点出现次数不止一次 减去就好了 最简单
		--e[pos].tie;
		--e[pos].siz;
		return;
	}
	if (!e[pos].son[0]) { //如果当前点是最小的了(没有左儿子)...
		e[e[pos].son[1]].fa = 0; //他右儿子弑父成王
		root = e[pos].son[1]; //根投靠了他
		if (!root) tot = 0; //如果原来那点没右儿子(只有他一个点) 就当做他遭天谴被劈没了吧..
	} else { //哼哼 如果有比他还小的 见上详细讲解 懒得打了
		e[e[pos].son[0]].fa = 0;
		int lax = find(e[pos].son[0],MAX);
		splay(lax);
		e[root].siz += e[e[pos].son[1]].siz;
		e[root].son[1] = e[pos].son[1];
		e[e[pos].son[1]].fa = root;
	}
	e[pos].v = 0; //清空原来的点的相关数据
	e[pos].tie = 0;
	e[pos].siz = 0;
	e[pos].fa = 0;
	e[pos].son[0] = 0;
	e[pos].son[1] = 0;
}
int rank(int p) //查找当前数排名
{
	int pos = find(root, p);
	splay(pos);
	return e[e[pos].son[0]].siz + 1;
} //根据他的某个孩子节点数推断他的排名 这里从小排所以是左儿子 当然别忘了 + 1
int k_th(int p) //查找排名第p的数
{
	int now = root; //从根找起 bot bottle 因为某东西实在太长 为了缩减 用该变量代替
	for (int bot = e[e[now].son[0]].siz ; p <= bot ||
		 p > bot + e[now].tie ; bot = e[e[now].son[0]].siz)
		if (p > bot + e[now].tie) {
			p = p - bot - e[now].tie;
			now = e[now].son[1];
		} else now = e[now].son[0];
	/*------------------------------------------------------
	首先now是根 如果他左儿子个数大于p 则说明第p名在左儿子里面
	如果左儿子个数加上他本身出现的个数 那就说明第p名在右儿子里面
	但这里要注意 p要相应地减去now点的左儿子个数和now点的出现次数 因为之后判断是根据新的儿子
	但之前的节点及那个节点的左儿子对他还有影响
	最后更新一下now 然后作为容器的bot也要随之更新
	------------------------------------------------------*/
	return e[now].v; //返回排名第p的点的值
}
int pred(int p) //找前驱 最接近他且严格小于他的点
{
	int pos = find(root,p); //先找到p的位置
	if (e[pos].v < p) return e[pos].v; //如果找到的不是p点 判断一下找到的点的大小 符合就输出
	splay(pos); //随时旋转
	return e[find(e[pos].son[0],MAX)].v;
} //从p的位置的左儿子找里面最大的 就是最接近p的 然后输出其值
int succ(int p) //找后继 最接近他且严格大于他的点
{
	int pos = find(root,p); //先找到p的位置
	if (e[pos].v > p) return e[pos].v; //如果找到的不是p点 判断一下找到的点的大小 符合就输出
	splay(pos); //随时旋转
	return e[find(e[pos].son[1],MIN)].v;
} //从p的位置的右儿子找里面最小的 就是最接近p的 然后输出其值
int main()
{
	int m,mode,x;
	scanf("%d",&m);
	while (m--)
	{
		scanf("%d%d",&mode,&x);
		switch (mode)
		{
			case 1:ins(x);break;
			case 2:del(x);break;
			case 3:printf("%d\n",rank(x));break;
			case 4:printf("%d\n",k_th(x));break;
			case 5:printf("%d\n",pred(x));break;
			case 6:printf("%d\n",succ(x));break;
		}
	}
}

OK了 开始攻向LCT!

猜你喜欢

转载自blog.csdn.net/Frocean/article/details/81607044