DS笔记
数据结构都快忘了,再学点。
以上的不会再写,只会再写别的。
DS都快忘了。
分块学习笔记不会再写,只会再写别的。
平衡树
众所周知,BST很方便,就是会退化成链或树很高,所以我们需要更高效的DS,那就是——平衡树。
Splay
Splay好闪,拜谢Splay。
Splay是通过不断将某个节点旋转至根来保持平衡,同时满足BST的性质。
怎么旋转呢?我们来画两张图!
左旋
这是一张图:
经过左旋后变为:
可以看出就是将 \(2\) 的右儿子 \(1\) 变为 \(2\) 的父亲,然后把 \(1\) 的左儿子变成了 \(2\) 的右儿子(也就是变为 \(1\) 的孙子)。
右旋
和左旋相反,下边给出图示(其实就是图片倒了一下):
经过右旋后变为:
同样可以看出,就是将 \(1\) 的左儿子 \(2\) 变为 \(1\) 的父亲,然后把 \(2\) 的右儿子变成了 \(1\) 的左儿子(也就是变为 \(2\) 的孙子)。
其实就是相反的。
某个节点的旋转就是这样的:
-
如果他是某个节点的左儿子,就右旋。
-
如果他是某个节点的右儿子,就左旋。
但是旋一次只会和父亲交换而不是到根,咋整?
答案是暴力。
是的,你没看错,只需要暴力转就可以了。
那不就写完了?溜了溜了。
别急!这是单旋Splay,它在棺材里躺得好好的呢!
单旋Splay很容易被卡到链。
如下图,如果有三点共线的情况,那么最后跳来跳去还是一条链,那咋办?
先转 \(2\) 再转 \(3\) 即可。
然后我们也可以得出规律了,就是先转父亲,再转自己一直这样直到自己变成根。
下面借一下大佬Enoch006的图,就是这样修改的:
接下来讲代码和操作啦。
先来声明一下变量捏。
siz//代表整棵Splay的大小
root//Splay的根节点
sz[i]//i的子树的大小
num[i]//i这个节点的值
cnt[i]//i这个节点的值出现的次数
fa[i]//i的父亲
son[i][0]//i的左儿子
son[i][1]//i的右儿子
清空
这个操作在删除后执行。
void all_zero(int x){
fa[x]=0;
son[x][0]=0;
son[x][1]=0;
num[x]=0;
cnt[x]=0;
sz[x]=0;
}
get
判断当前的点是左儿子还是右儿子(旋转要用)。
int get(int x){
if(x==son[fa[x]][0]){
return 0;
}return 1;
}
pushup
随便写的名字
用于修改后确认并修改树的大小。
void pushup(int x){
if(x>=1){
sz[x]=cnt[x];
if(son[x][0]>=1){
sz[x]+=sz[son[x][0]];
}if(son[x][1]>=1){
sz[x]+=sz[son[x][1]];
}
}
}
rotate
如下图。
我们要旋转 \(4\),就会变成下面的:
一开始 \(4\) 的父亲是 \(2\),\(2\) 的父亲是 \(1\)
设 flag
为 get(4)
,那么就是 \(4\) 是左儿子还是右儿子。
那么可以发现,\(2\) 的 flag
儿子和 \(4\) 的 flag^1
儿子还有 \(1\) 的或左或右的儿子(就是看一开始 \(2\) 是哪个儿子)都要换。
其实旋转就是连边、断边。
(下面的数字就是上面那棵树的节点)。
于是我们先把 \(2\) 和 \(6\) 连上,再连 \(4\) 和 \(2\),最后连 \(1\) 和 \(2\)。
那么旋转就好了。
void rotate(int x){
int f=fa[x],flag=get(x);
int gra=fa[f];
son[f][flag]=son[x][flag^1];
fa[son[f][flag]]=f;
son[x][flag^1]=f;
fa[f]=x,fa[x]=gra;
if(gra>=1){
if(son[gra][1]==f){
son[gra][1]=x
}else{
son[gra][0]=x
}
}pushup(f),pushup(x);
}
Splay
双旋的关键。
这里判断两种情况:
-
三点一线,这样就是上面说的,先转父亲,再转自己一直这样直到自己变成根。
-
不是三点一线,
疯狂暴力旋转即可。
void Splay(int x){
for(int f=0,f=fa[x];rotate(x)){
if(fa[f]>=1){
if(get(x)==get(fa)){
rotate(fa);
}else{
rotate(x);
}
}
}rt=x;
}
insert
也是分类讨论:
-
如果
rt==0
,那么树肯定是空的,那么
咕了。