平衡树学习笔记

二叉搜索树

平衡树的基础。

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

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

基础操作:

先来规定一下变量名:

\(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

全名伸展树。

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

反正复杂度是对的。

基础操作:

旋转操作:

我们要让 \(x\)\(y\) 的爹,但是又要满足二叉搜索树的性质。

现在 \(x\)\(y\) 的左儿子,所以 \(val_x < val_y\)

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

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

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

\(x\) 的爹就变成了 \(z\)

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

大概总结一下这个规律:

\(y\)\(x\) 的爹, \(z\)\(y\) 的爹。

如果说 \(x\)\(y\)\(id\) 号儿子。

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

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

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

  • 所以说 \(y\) 总是会当 \(x\)\(id \wedge 1\) 号儿子。

然后 \(x\)\(id ^ 1\) 号儿子因为 \(y\) 的到来,要变成 \(y\)\(id\) 号儿子。

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

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); // 旋转到根节点,维护树的平衡
}

插入操作:

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

如果这个位置存在,\(cnt\)\(1\),然后返回。

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

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\) 分裂,分裂成两颗子树 \(x\)\(y\)\(x\) 上的值都 \(\le v\)\(y\) 上的值都 \(> v\)

从根节点开始,如果当前节点的权值 \(\le v\),说明他的左子树也 \(\le v\),但是右子树可能有一部分 \(\le 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) \le 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\) 按照权值 \(v - 1\) 分裂成 \(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\),按照 \(v - 1\) 分裂成 \(x,y\)\(x\) 子树上的都是比他小的,再加一即为答案。

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\le $左子树的大小,说明了在左子树上,去左子树找,如果左子树大小加一等于 \(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 操作:

按照权值 \(v - 1\) 分裂成 \(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;
}
posted @ 2022-08-17 10:43  TLE_Automation  阅读(39)  评论(2)    收藏  举报