寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄寄|

TLE_Automation

园龄:2年9个月粉丝:19关注:23

平衡树学习笔记

二叉搜索树

平衡树的基础。

二叉搜索树是一种二叉树,并且满足左儿子的值小于根的值,右儿子的值大于根的值。

并且二叉搜索树的子树也是二叉搜索树。

基础操作:

先来规定一下变量名:

siz[now] 表示以 now 为根的子树大小,ls[now] 表示 now 的左儿子,rs[now] 表示 now 的右儿子。

val[now] 表示 now 节点的权值,cnt[now] 表示 now 节点权值的数量。

遍历操作:

如果没有儿子了直接返回,然后遍历左右儿子输出。

void print(int now) {
if(!now) return;
print(lc[now]);
for(int i = 1; i <= cnt[now]; i++)
printf("%d ", val[now]);
print(rc[now]);
}

查找最值操作:

因为左儿子是最小的,我们从根节点出发,一直找左儿子,最终找到的左儿子所代表的权值就是最小值。

因为右儿子是最小的,我们从根节点出发,一直找右儿子,最终找到的右儿子所代表的权值就是最大值。

int findMin(int now) {
if(!lc[now]) return now;
return findMin(ls[now]);
}
int findMax(int now) {
if(!rc[now]) return now;
return findMax(rs[now]);
}

插入操作:

考虑从根节点出发,找属于他的位置。

如果当前没有这个节点,说明需要新建节点。

如果有的话,就根据二叉搜索树的性质。

如果 v 比当前的值小,去左儿子找。

如果 v 跟当前的值相等了,个数加 1,然后返回。

如果 v 比当前的值大,去右儿子找。

void Insert(int &now, int v) {
if(!now) {
now = ++idx;
val[now] = v, cnt[now] = siz[now] = 1;
return;
}
siz[now]++;
if(val[now] > v) Insert(ls[now], v);
if(val[now] == v) return cnt[now]++, void();
if(val[now] < v) Insert(rs[now], v);
}

删除操作:

如果是删除最值操作。

如果是删除最小值,就是一直遍历到最后,如果说是叶子节点,就直接删除。

如果不是叶子节点,因为是找最小值再删除,我们先一直遍历左儿子,到了没有左儿子的节点,将这个节点删除就是将这个节点直接变成自己的右儿子。

删除最大值同理。

int deletemin(int &now) {
if(!ls[now]) {
int u = now; now = rs[now];
return u;
}
else {
int u = deletemin(lc[now]);
siz[now] -= cnt[u];
return u;
}
}
int deletemax(int &now) {
if(!rs[now]) {
int u = now; now = ls[now];
return u;
}
else {
int u = deletemax(rc[now]);
siz[now] -= cnt[u];
return u;
}
}

如果是随便删除一个权值为 v 的点呢。

我们从根节点出发,根据二叉搜索树的性质,去找到 v 所在的位置。

如果这个地方 cnt[now]>1 直接减 1,然后返回。

否则如果这个节点只有一个儿子,那么直接加这个节点设成他的儿子。

如果有两个儿子,就将这个节点变成左儿子的最大值或者右儿子的最小值。

void Del(int &now, int v) {
siz[now]--;
if(val[now] == v) {
if(cnt[now] > 1) {
cnt[now]--; return;
}
if(ls[now] && rs[now]) now = deletemin(rs[now]);
else now = ls[now] + rs[now];
return;
}
if(val[now] > v) Del(ls[now], v);
if(val[now] < v) Del(rs[now], v);
}

求排名操作:

从根节点出发,根据权值不断的找即可。

求元素排名:

int Rank(int now, int v) {
if(val[now] == v) return siz[ls[now]] + 1;
if(val[now] > v) return Rank(ls[now], v);
if(val[now] < v) return Rank(rs[now], v) + siz[ls[now]] + cnt[now];
}

求排名的元素:

int kth(int now, int k) {
if(siz[ls[now]] >= k) return kth(ls[now], k);
if(siz[ls[now]] < k - cnt[now]) return kth(rs[now], k - siz[ls[now]] - cnt[now]);
return val[now];
}

Splay

全名伸展树。

是一种通过玄学旋转来实现树的平衡的树。

反正复杂度是对的。

基础操作:

旋转操作:

我们要让 xy 的爹,但是又要满足二叉搜索树的性质。

现在 xy 的左儿子,所以 valx<valy

所以 x 想当爹的话 y 就要成为 x 的右儿子。

但是 x 的右儿子上已经有人了。

于是我们考虑 x,B,y 的关系: y>B>x

x 的爹就变成了 z

所以我们直接把 y 的左儿子变成 B,这样就完成了旋转。

大概总结一下这个规律:

yx 的爹, zy 的爹。

如果说 xyid 号儿子。

如果说 id=1 说明是右儿子, id=0 就是左儿子。

如果 x 是左儿子,说明了 y 要当 x 的右儿子。

如果说 x 是右儿子,说明了 y 要当 x 的左儿子。

  • 所以说 y 总是会当 xid1 号儿子。

然后 xid1 号儿子因为 y 的到来,要变成 yid 号儿子。

最后更新父节点的信息,更新旋转后节点的子树大小即可。

void moveroot(int x) {
int y = tree[x].fa;
int z = tree[y].fa;
int k = (tree[y].ch[1] == x); // x 是 y 的哪个儿子
tree[z].ch[tree[z].ch[1] == y] = x; // 更新 z 的儿子是 x
tree[x].fa = z; // x 的爹是 z
tree[y].ch[k] = tree[x].ch[k ^ 1]; // x 的原来要变成 y 的那个儿子成为了 y 的儿子。
tree[tree[x].ch[k ^ 1]].fa = y; // 更新爹
tree[x].ch[k ^ 1] = y; // y 成为了 x 的子树
tree[y].fa = x; // y 的爹是 x
}

如果你觉得这样就能使树平衡就大错特错了,还是有数据可以hack的。

依旧是上面那个图。

按照要求不断的旋转 x 节点。

然后继续旋转 x 节点。

然后你就发现,这不是寄了,旋转完之后还是一条链。

本身上是因为让 x 不断的旋转,结果 x,y,z 是一条链,然后不断的旋转只是把 y,z 移动他的同一个儿子罢了。

所以我们先转一下 y 节点,然后不断的去转 x 就行了。

先旋转 y 节点。

然后旋转 x 节点。

void splay(int x, int goal) {
while(tree[x].fa != goal) { // 当他的爹不是目标节点的时候
int y = tree[x].fa, z = tree[y].fa;
if(z ^ goal) // 为啥防止转两遍然后把 x 直接转到目标
(tree[z].ch[1] == y) ^ (tree[y].ch[1] == x) ? moveroot(x) : moveroot(y); // 判断是否是链
moveroot(x);
}
if(!goal) root = x; // 更新根节点
}

查找操作:

我们从根节点出发,如果树是空的直接返回。

否则去找那个儿子,根据权值的大小去找儿子。

最后为了后面的求前驱后继操作,把找到的这个节点转到根节点。

void find(int x) {
int u = root;
if(!u) return; // 如果是空树
while(tree[u].ch[x > tree[u].val] && x != tree[u].val) // 找符合条件的儿子跳过去
u = tree[u].ch[x > tree[u].val];
splay(u, 0); // 旋转到根节点,维护树的平衡
}

插入操作:

我们从根节点出发,找到他应该插入的位置。

如果这个位置存在,cnt1,然后返回。

如果说不存在,新建一个节点。

void Insert(int x) {
int u = root, fa = 0;
while(u && tree[u].val != x)
fa = u, u = tree[u].ch[x > tree[u].val];
if(u) tree[u].cnt++;
else {
u = ++idx;
if(fa) tree[fa].ch[x > tree[fa].val] = u;
tree[idx].fa = fa, tree[idx].val = x, tree[idx].cnt = 1, tree[idx].siz = 1;
}
splay(u, 0);
}

前驱后继操作:

先找到这个位置,但是不一定能直接找到,先查找这个位置,然后把他变成根,这样前驱就是左子树最大值。

后继就是右子树最小值。

int Next(int x, int f) {
find(x);
int u = root;
if(tree[u].val > x && f) return u;
if(tree[u].val < x && !f) return u;
u = tree[u].ch[f];
while(tree[u].ch[f ^ 1]) u = tree[u].ch[f ^ 1];
return u;
}

删除操作:

首先找到这个数的前驱,把他Splay到根节点。
然后找到这个数后继,把他旋转到前驱的底下。
比前驱大的数是后继,在右子树。
比后继小的且比前驱大的有且仅有当前数。
在后继的左子树上面。
因此直接把当前根节点的右儿子的左儿子删掉就可以啦。

void Delete(int x) {
int last = Next(x, 0);
int nxt = Next(x, 1);
splay(last, 0), splay(nxt, last);
int del = tree[nxt].ch[0];
if(tree[del].cnt > 1) {
tree[del].cnt--, splay(del, 0);
}
else tree[nxt].ch[0] = 0;
}

FHQ-Treap

码量短的平衡树,几分钟就能写完,并且特别好调,推荐学习这种。

split 操作:

分裂操作,按值 v 分裂,分裂成两颗子树 xyx 上的值都 vy 上的值都 >v

从根节点开始,如果当前节点的权值 v,说明他的左子树也 v,但是右子树可能有一部分 v,所以去递归处理右子树,同时为了找到分裂后真正的 p 的右儿子。

>v 的时候一样。

inline void split(int p, int v, int &x, int &y) {
if(!p) {x = y = 0; return;}
if(tr[p].val <= v) x = p, split(rs(x), v, rs(x), y);
else y = p, split(ls(y), v, x, ls(y));
pushup(p);
}

merge 操作:

分裂操作的逆操作,将两颗子树按照随机的 key 值合并起来,如果 key(x)key(y),按照小根堆, x 需要在 y 的上面,因为 x 的权值小于 y 的权值,所以只需要把 rs(x)y 合并起来当做 x 的右儿子就行了。

inline int merge(int x, int y) {
if(!x || !y) return x + y;
if(tr[x].key < tr[y].key) { rs(x) = merge(rs(x), y); pushup(x); return x;}
else {ls(y) = merge(x, ls(y)), pushup(y); return y; }
}

insert 操作:

插入一个权值为 v 的点,先按权值 v 分裂成 x,y,然后新建一个权值为 v 的节点,把 x,z 合并起来,再和 y 合并。

inline void Insert(int v) {
split(root, v, x, y);
z = newnode(v);
root = merge(merge(x, z), y);
}

delete 操作:

删除一个权值为 v 的点,先按权值 v 分裂成 x,z,再把 x 按照权值 v1 分裂成 y,这个时候 y 的值全部等于 v,将 y 的根删除掉,也就是直接将 y 的 左右儿子合并起来。

inline void del(int v) {
split(root, v, x, z), split(x, v - 1, x, y);
y = merge(ls(y), rs(y)); root = merge(merge(x, y), z);
}

getrank 操作:

找权值为 v 的点的排名,也就是比他小的数的个数再加 1,按照 v1 分裂成 x,yx 子树上的都是比他小的,再加一即为答案。

inline int getrank(int v) {
split(root, v - 1, x, y);
res = tr[x].siz + 1;
root = merge(x, y); return res;
}

getnum 操作:

找排名为 k 的点的权值,从根节点出发,如果 k左子树的大小,说明了在左子树上,去左子树找,如果左子树大小加一等于 k,说明当前点为要找的点,否则去右子树找,要减去左子树大小和根节点的大小。

inline int getnum(int p, int k) {
if(tr[ls(p)].siz >= k) return getnum(ls(p), k);
else if(tr[ls(p)].siz + 1 == k) return p;
else return getnum(rs(p), k - tr[ls(p)].siz - 1);
}

getpre 操作:

按照权值 v1 分裂成 x,y,找到 x 上最大的数。

inline int getpre(int v) {
split(root, v - 1, x, y), res = getnum(x, tr[x].siz), root = merge(x, y); return tr[res].val;
}

getnxt 操作:

按照权值 v 分裂成 x,y,找到 y 上最小的数。

inline int getnxt(int v) {
split(root, v, x, y), res = getnum(y, 1), root = merge(x, y); return tr[res].val;
}

本文作者:TLE_Automation

本文链接:https://www.cnblogs.com/tttttttle/p/16594240.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   TLE_Automation  阅读(28)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起