平衡树

平衡树

满足等式:

\[\Huge{Treap=Tree+heap} \]

因此 \(\text{Treap}\) 树堆其实就是树+堆。树是二叉查找树 \(\text{BST}\),堆是二叉堆,大根堆小根堆都可以。

二叉查找树BST

就是一棵二叉树,满足一个条件:左子树所有节点<父节点<右子树所有节点

因此,二叉树的中序遍历显然就是一个单调递增或递减的数列

同时,二叉查找树还有一些很好的性质,方便我们寻找一个数的位置,期望复杂度是 \(O(log n)\)

如:

插入一个数,删去一个数,查找一个数的rk,找一个数的前驱和后继

这就是一个合法的 \(BST\)

heap

heap就是堆,可以是大根堆也可以是小根堆,看自己的需要

Treap

对于 \(\text{Treap}\) 来说,我们用二叉查找树的搜索顺序,但是我们每个节点的值让它都满足的堆的性质

我们一般为了保证 \(\text{Treap}\) 的平衡性,不得不对于每个点随机赋值

每次都随机一个权值作为它的附加权值,那么它就大概率是平衡的。

fhq Treap(非旋treap)

一般的平衡树都会有很恼人的旋转,又不好写,又不好调,但是fhq大佬解决了这个问题。

fhq-treap(又称xixike-treap)

注意:这里的 \(val\) 满足 \(\text{BST}\) \(rnd\) 满足堆的性质

对于上面两个图来说,\(val_1<val_2\) 如果不看\(rnd\) 值的话,那么两种方式其实是都满足的,但是由于 \(rnd\) 的堆性质,里面必定会有一个满足 1 和 2 之间的 \(rnd\) 关系

核心思想有两个:split 和 merge,表示裂开和合并

\(\text{Google}\) 翻译:split 分离,分裂

它的主要思想就是把一个Treap分成两个。

split操作有两种类型,一种是按照权值分配,一种是按前k个分配。

第一种就是把所有小于k的权值的节点分到一棵树中,第二种是把前k个分到一个树里。

我们提前声明一个 \(\mathrm{update}\) 函数来更新某个点的 \(siz\) 大小

权值版:

inline void split(int p, int k, int& rt1, int& rt2) {
    if (!p) return rt1 = rt2 = 0, void();
    if (tr[p].key <= k) rt1 = p, split(tr[p].r, k, tr[p].r, rt2);
    else rt2 = p, split(tr[p].l, k, rt1, tr[p].l);
    update(p);
}

这样写的话我们最后的 \(rt_1\) 就是小于等于 \(k\) 的数的子树的根,同理 \(rt_2\) 就是大于 \(k\) 的数的子树的根

对于我们遍历到每一个点,假如它的权值小于 \(k\),那么它的所有左子树,都要分到左边的树里,然后遍历它的右儿子。假如大于 \(k\),把它的所有右子树分到右边的树里,遍历左儿子。

因为它的最多操作次数就是一直分到底,效率就是 \(O(log ~ n)\)

对于前 \(k\) 个版的,就是像找第 \(k\) 大的感觉。每次减掉 siz

inline void split(int p, int k, int& rt1, int& rt2) {
    if (!p) return rt1 = rt2 = 0, void();
    if (tr[p].siz <= k) rt1 = p, split(tr[p].r, k, tr[p].r, rt2);
    else rt2 = p, split(tr[p].l, k - tr[p].siz - 1, rt1, tr[p].l);
    update(p);
}

这就是我们的 \(\mathrm {split}\) 操作了,它可以把一棵树分成左子树和右子树

然后就是 \(\mathrm{merge}\) 操作,这个的话我们还是需要一些特殊的方式合并,由于我们合并时无法统一树高,所以我们核心是以 \(rand\) 值进行 \(\mathrm{merge}\) 的,合并过程中一定要始终保持 \(rand\) 值大的在前面另一个直接作为前一个的儿子和其原来的儿子进行合并即可。

我们写 \(\mathrm{merge}\) 之前一定要牢记一点,子树 \(rt_1\)\(key\) 一定要小于子树 \(rt_2\)\(key\) 的时候你才可以 \(\mathrm{merge}\)

inline int merge(int rt1, int rt2){
    if (!rt1 || !rt2) return rt1 + rt2;
    if (tr[rt1].val > tr[rt2].val) {
        tr[rt1].r = merge(tr[rt1].r, rt2);
        update(rt1);
       	return rt1;
    }
    else {
      	tr[rt2].l = merge(rt1, tr[rt1].l);
        update(rt2);
        return rt2;
    }
}

返回的每个值都是根

以上两个操作,就是 \(\mathrm{PHQ-treap}\) 的精髓了,其余所有操作都是基于这两个操作展开的

首先是插入一个数 \(\mathrm{insert}\) 插入一个权值为 \(k\) 的点,可以先让原树裂开,然后把它和权值小于等于 \(k\) 的那棵树 \(\mathrm{merge}\),然后再用 \(\mathrm{merge}\) 后的那棵树进行二次 \(\mathrm{merge}\),那么代码应该长成这样

inline void insert(int k) {
    split(root, k, x, y);
    root = merge(merge(x, new_node(k)), y);
}

补充一下 new_node 函数,这个函数是说我们新开一个点,那么应该很容易:

struct node {
    int key, val, siz;
    int l, r;
}tr[N << 1];
inline void new_node(int k) {
    tr[++cnt] = (node){k, rnd(), 1, 0, 0}
}

然后是如何删除一个权值为 \(k\) 的点

首先是把按照权值小于等于 \(k\) 和大于 \(k\) 裂开成 \(x\) 子树和 \(y\) 子树,然后再让 \(x\) 子树裂开成小于 \(k\) 和等于 \(k\) 的子树 \(x\)\(z\),然后我们直接让 \(y\) 的两个儿子 \(\mathrm{merge}\) 一下,然后再 merge(merge(x, y), z),就好了

如果是删除多个的话直接合并 \(x\)\(z\) 即可

inline void delete(int k) {
    split(root, k, x, y);
    split(x, k - 1, x, y);
    y = merge(tr[y].l, tr[y].r);
    root = merge(merge(x, y), z);
}

然后是 get_rank 函数,就是查询小于等于某个权值的有多少个,直接split一下查一下,然后再merge回去就好

inline void get_rank(int k) {
    split(root, k - 1, x, y);
    write(tr[x].siz + 1, '\n');
    root = merge(x, y);
}

然后是 get_key 函数,查询某个排名对应的权值,这个有一点麻烦,首先我们还是想着如何通过 splitmerge 来get到排名对应的权值,一个最简单的办法就是,我们循环的调用子树的左右两边的 siz 即可

inline int get_key(int rank) {
    int p = root;
    while (p) {
        if (tr[tr[p].l].siz + 1 == rank) break;
        if (tr[tr[p].l].siz >= rank) p = tr[p].l;
        else rank -= tr[tr[p].l].siz, p = tr[p].r;
    }
    return tr[p].key;
}

然后是查小于 \(k\) 的最大的数,或者称为前驱,我们先把小于 \(k\) 的一部分先裂开,然后一直往右找到底即可

inline int get_prev(int k) {
    split(root, k - 1, x, y);
    int p = x;
    while (tr[p].r) p = tr[p].r;
    int ret = tr[p].key;
    root = merge(x, y);
    return ret;
}

同理是查大于 \(k\) 的最大的数,或者称为后驱,我们先把大于 \(k\) 的一部分裂开,然后一路往左边找

inline int get_next(int k) {
    split(root, k - 1, x, y);
    int p = y;
    while (tr[p].l) p = tr[p].l;
    int ret = tr[p].key;
    root = merge(x, y);
    return ret;
}

这就是最基本的操作们了!

我们也因此可以完成 P3369【模板】普通平衡树

/*
Blackpink is the Revolution
light up the sky
Blackpink in your area
*/
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cctype>
#include <bitset>
#include <vector>
#include <cstdio>
#include <random>
#include <cmath>
#include <queue>
#include <ctime>
#include <map>
#include <set>
#define rep(i, a, b) for(int i = (a); (i) <= (b); ++i)
#define per(i, a, b) for(int i = (a); (i) >= (b); --i)
#define whlie while
using namespace std;
using ll = long long;
using P = pair<int ,int>;
namespace scan {
template <typename T>
inline void read(T &x) {
    x = 0; char c = getchar(); int f = 0;
    for (; !isdigit(c); c = getchar()) f |= (c == '-');
    for (; isdigit(c); c=getchar()) x = x * 10 + (c ^ 48);
    if (f) x = -x;
}
template <typename T, typename ...Args>
inline void read(T &x, Args &...args) {
    read(x), read(args...);
}
template <typename T>
inline void write(T x, char ch) {
    if (x < 0) putchar('-'), x = -x;
    static short st[30], tp;
    do st[++tp] = x % 10, x /= 10; while(x);
    while (tp) putchar(st[tp--] | 48);
    putchar(ch);
}
template <typename T>
inline void write(T x) {
    if (x < 0) putchar('-'), x = -x;
    static short st[30], tp;
    do st[++tp] = x % 10, x /= 10; while(x);
    while(tp) putchar(st[tp--] | 48);
}
inline void write(char ch){
    putchar(ch);
}
template <typename T, typename ...Args>
inline void write(T x, char ch, Args ...args) {
    write(x, ch), write(args...);
}
} //namespace scan

using namespace scan;
mt19937 rnd(114514);
const int mod = 1e9 + 7;
const int N = 4e5 + 5;
int n, m, T, ans, op, root, x, y, z, cnt;

namespace RevolutionBP {

struct node {
    int key, val, siz, l, r;
}tr[N << 2];

inline void update(int p) {tr[p].siz = tr[tr[p].l].siz + tr[tr[p].r].siz + 1;}
inline int new_node(int key) {tr[++cnt] = (node){key, rnd(), 1, 0, 0}; return cnt;}

inline void split(int p, int k, int& x, int& y) {
    if (!p) return x = y = 0, void();
    if (tr[p].key <= k) x = p, split(tr[p].r, k, tr[p].r, y);
    else y = p, split(tr[p].l, k, x, tr[p].l);
    update(p);
}

inline int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (tr[x].val <= tr[y].val) return tr[y].l = merge(x, tr[y].l), update(y), y;
    else return tr[x].r = merge(tr[x].r, y), update(x), x;
}

inline void insert(int key) {
    split(root, key, x, y);
    root = merge(merge(x, new_node(key)), y);
}

inline void Delete(int key) {
    split(root, key, x, z);
    split(x, key - 1, x, y);
    y = merge(tr[y].l, tr[y].r);
    root = merge(merge(x, y), z);
}

inline void get_rank(int k) {
    split(root, k - 1, x, y);
    write(tr[x].siz + 1, '\n');
    root = merge(x, y);
}

inline int get_key(int rank) {
    int p = root;
    while (p) {
        if (tr[tr[p].l].siz + 1 == rank) break;
        if (tr[tr[p].l].siz < rank) rank -= tr[tr[p].l].siz + 1, p = tr[p].r;
        else p = tr[p].l;
    }
    return tr[p].key;
}

inline int get_prev(int k) {
    split(root, k - 1, x, y);
    int p = x;
    while (tr[p].r) p = tr[p].r;
    int ret = tr[p].key;
    root = merge(x, y);
    return ret;
}


inline int get_next(int k) {
    split(root, k   , x, y);
    int p = y;
    while (tr[p].l) p = tr[p].l;
    int ret = tr[p].key;
    root = merge(x, y);
    return ret;
}

void main() {
    read(T);
    while (T--) {
        read(op, m);
        if (op == 1) insert(m);
        if (op == 2) Delete(m);
        if (op == 3) get_rank(m);
        if (op == 4) write(get_key(m), '\n');
        if (op == 5) write(get_prev(m), '\n');
        if (op == 6) write(get_next(m), '\n');
    }
    return void();
}
/*
插入 x 数
删除 x 数(若有多个相同的数,因只删除一个)
查询 x 数的排名(排名定义为比当前数小的数的个数 +1+1 )
查询排名为 x 的数
求 x 的前驱(前驱定义为小于 x,且最大的数)
求 x 的后继(后继定义为大于 x,且最小的数)
*/
}
signed main(){
    RevolutionBP::main();
    return 0;
}
//write: RevolutionBP

splay

首先我们假设一种情况,如果我们面临着一个神奇的东西:就是一棵 treap 长成这 B

这就会很难看并且影响复杂度,我可不想每次都这样去查一个最低端的数,还不如写暴力呢,于是我们就考虑插入点的时候能不能用一些美妙的方式使得这棵树长得尽可能平衡呢?

我们选择 rotate 操作,我们可以通过此操作在保证 \(\mathrm{treap}\) 的性质下进行 rotate 操作

旋转是平衡树最主要的操作,其本质在于,每次进行旋转时,左右子树当中之一高度 -1,另外一棵高度 +1,以达到平衡的目的。

这个操作分成两类,一类是左旋,一类是右旋

左旋:

第一次连边,节点x的子节点成为x的父节点的右子节点

第二次连边,节点x成为节点x的父节点的父节点的子节点,方向与x的父节点相同

第三次连边,节点x的父节点成为节点x的左子节点

右旋:

第一次连边,节点x的子节点成为x的父节点的左子节点

第二次连边,节点x成为节点x的父节点的父节点的子节点,方向与x的父节点相同

第三次连边,节点x的父节点成为节点x的右子节点

code:

struct Blance_tree {
	int val, cnt, ch[2], fa, siz;
}tr[N << 1];

inline void push_up(int x) {tr[x].siz = tr[tr[x].ch[0]].siz + tr[tr[x].ch[1]].siz + tr[x].cnt;}

inline int get(int x) {return tr[tr[x].fa].ch[1] == x;}

inline int new_node(int x, int f) {
	tr[++tot].val = x, tr[tot].fa = f;
	tr[tot].cnt = tr[tot].siz = 1;
	tr[tot].ch[0] = tr[tot].ch[1] = 0;
	return tot;
}

inline void rotate(int x) {
	int y = tr[x].fa, z = tr[y].fa, k = get(x);
	tr[y].ch[k] = tr[x].ch[k ^ 1];
	if (tr[x].ch[k ^ 1]) tr[tr[x].ch[k ^ 1]].fa = y;
	tr[x].ch[k ^ 1] = y;
	tr[y].fa = x;
	if (z) tr[z].ch[y == tr[z].ch[1]] = x;
	tr[x].fa = z;
	push_up(x), push_up(y);
}

然后就是我们的 splay 操作,具体来说就是若干次 rotate 操作的合体版,或者说,Splay 要求:每访问一个节点后都要强制将其旋转到根节点。此时旋转操作具体分为 6 种情况讨论(其中 x 为需要旋转到根的节点)

第一二种是 \(fa_x\) 是根节点,那么直接左旋和右旋即可

第三四种是 \(fa_x\) 不是根节点,但是 \(x\)\(fa_x\)都是左儿子或者都是右儿子,那么先让 \(fa_x\) 左旋或者右旋,再让 \(x\) 左旋或者右旋

第五六种是 \(fa_x\) 不是根节点,但是 \(x\)\(fa_x\) 一个左儿子一个右儿子,那么先让 \(x\) 左旋再右旋或者右旋再左旋即可

inline void Splay(int x, int k) {
	while (tr[x].fa != k) {
		int y = tr[x].fa, z = tr[y].fa;
		if (z != k) rotate((tr[y].ch[0] == x) ^ (tr[z].ch[0] == y) ? x : y);
		rotate(x);
	}
	if (!k) rt = x;
}

代码就一行…

然后是插入操作,这个的话就是前面的 fhq 比较像了,如果树是空的,直接插入,不然的话,我们直接一路按照二叉搜索树的性质递归的去找即可

inline void insert(int k) {
	int u = rt, f = 0;
	while (u and tr[u].val != k) f = u, u = tr[u].ch[k > tr[u].val];
	// cout << "???";
	if (u) ++tr[u].cnt;
	else {
		u = new_node(k, f);
		if (f) tr[f].ch[k > tr[f].val] = u;
	}
	Splay(u, 0);
}

然后我们就可以根据这个来进行平衡树的一系列操作了

找到某个值对应的节点,并且给它转上去

inline void find(int k) {
	int u = rt;
	if (!u) return ;
	while (tr[u].ch[k > tr[u].val] && k != tr[u].val) u = tr[u].ch[k > tr[u].val];
	Splay(u, 0);
}

查一个点的排名:
其实是比较容易的,查左边,比中间,差右边即可

inline int get_rank(int k) {
    Find(k);
    return tr[tr[rt].ch[0]].siz;
}

查一个排名对应的值

我们仿照上面的步骤,和子树进行比较即可

inline int kth(int k) {
	int u = rt;
	while (1) {
		if (k <= tr[tr[u].ch[0]].siz) u = tr[u].ch[0];
		else {
			k -= tr[tr[u].ch[0]].siz + tr[u].cnt;
			if (k <= 0) return tr[u].val;
			u = tr[u].ch[1];
		}
	}
}

查一个点的前驱和后继

\(x\) 旋转到根,先往左然后一直往右

inline int Pre(int x) {
	find(x);
	int u = rt;
	if (tr[u].val < x) return u;
	u = tr[rt].ch[0];
	while (tr[u].ch[1]) u = tr[u].ch[1];
	return u;
}

先往右然后一直往左

inline int Next(int x) {
	find(x);
	int u = rt;
	if (tr[u].val > x) return u;
	u = tr[rt].ch[1];
	while (tr[u].ch[0]) u = tr[u].ch[0];
	return u;
}

删除一个数

inline void Delete(int x) {
	int last = Pre(x);
	int next = Next(x);
	Splay(last, 0);
	Splay(next, last);
	int del = tr[next].ch[0];
	if (tr[del].cnt > 1) {
		tr[del].cnt--;
		Splay(del, 0);
	}
	else tr[next].ch[0] = 0;
}

以上就是全部思路了

然后我们就可以写出模板题的做题思路

/*
Blackpink is the Revolution
light up the sky
Blackpink in your area
*/
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cctype>
#include <bitset>
#include <vector>
#include <cstdio>
#include <cmath>
#include <queue>
#include <ctime>
#include <map>
#include <set>
#define int long long
#define rep(i, a, b) for(int i = (a); (i) <= (b); ++i)
#define per(i, a, b) for(int i = (a); (i) >= (b); --i)
using namespace std;
using ll = long long;
using P = pair<int ,int>;
namespace scan {

template <typename T>
inline void read(T &x) {
	x = 0; char c = getchar(); int f = 0;
	for (; !isdigit(c); c = getchar()) f |= (c == '-');
	for (; isdigit(c); c=getchar()) x = x * 10 + (c ^ 48);
	if (f) x = -x;
}

template <typename T, typename ...Args>
inline void read(T &x, Args &...args) {
	read(x), read(args...);
}

template <typename T>
inline void write(T x, char ch) {
	if (x < 0) putchar('-'), x = -x;
	static short st[30], tp;
	do st[++tp] = x % 10, x /= 10; while(x);
	while (tp) putchar(st[tp--] | 48);
	putchar(ch);
}

template <typename T>
inline void write(T x) {
	if (x < 0) putchar('-'), x = -x;
	static short st[30], tp;
	do st[++tp] = x % 10, x /= 10; while(x);
	while(tp) putchar(st[tp--] | 48);
}

inline void write(char ch){
	putchar(ch);
}

template <typename T, typename ...Args>
inline void write(T x, char ch, Args ...args) {
	write(x, ch), write(args...);
}

} //namespace scan

using namespace scan;
const int mod = 1e9 + 7;
const int N = 4e5 + 5;
const int inf = 2e9;
int n, m, T, ans;
int a[N];

int rt, opt, x, tot;

struct Blance_tree {
	int val, cnt, ch[2], fa, siz;
}tr[N << 1];

inline void push_up(int x) {tr[x].siz = tr[tr[x].ch[0]].siz + tr[tr[x].ch[1]].siz + tr[x].cnt;}

inline int get(int x) {return tr[tr[x].fa].ch[1] == x;}

inline int new_node(int x, int f) {
	tr[++tot].val = x, tr[tot].fa = f;
	tr[tot].cnt = tr[tot].siz = 1;
	tr[tot].ch[0] = tr[tot].ch[1] = 0;
	return tot;
}

inline void rotate(int x) {
	int y = tr[x].fa, z = tr[y].fa, k = get(x);
	tr[y].ch[k] = tr[x].ch[k ^ 1];
	if (tr[x].ch[k ^ 1]) tr[tr[x].ch[k ^ 1]].fa = y;
	tr[x].ch[k ^ 1] = y;
	tr[y].fa = x;
	if (z) tr[z].ch[y == tr[z].ch[1]] = x;
	tr[x].fa = z;
	push_up(x), push_up(y);
}

inline void Splay(int x, int k) {
	while (tr[x].fa != k) {
		int y = tr[x].fa, z = tr[y].fa;
		if (z != k) rotate((tr[y].ch[0] == x) ^ (tr[z].ch[0] == y) ? x : y);
		rotate(x);
	}
	if (!k) rt = x;
}

inline void find(int k) {
	int u = rt;
	if (!u) return ;
	while (tr[u].ch[k > tr[u].val] && k != tr[u].val) u = tr[u].ch[k > tr[u].val];
	Splay(u, 0);
}

inline void insert(int k) {
	int u = rt, f = 0;
	while (u and tr[u].val != k) f = u, u = tr[u].ch[k > tr[u].val];
	// cout << "???";
	if (u) ++tr[u].cnt;
	else {
		u = new_node(k, f);
		if (f) tr[f].ch[k > tr[f].val] = u;
	}
	Splay(u, 0);
}

inline int get_rank(int k) {
	find(k);
	return tr[tr[rt].ch[0]].siz;
}

inline int kth(int k) {
	int u = rt;
	while (1) {
		if (k <= tr[tr[u].ch[0]].siz) u = tr[u].ch[0];
		else {
			k -= tr[tr[u].ch[0]].siz + tr[u].cnt;
			if (k <= 0) return tr[u].val;
			u = tr[u].ch[1];
		}
	}
}

inline int Pre(int x) {
	find(x);
	int u = rt;
	if (tr[u].val < x) return u;
	u = tr[rt].ch[0];
	while (tr[u].ch[1]) u = tr[u].ch[1];
	return u;
}

inline int Next(int x) {
	find(x);
	int u = rt;
	if (tr[u].val > x) return u;
	u = tr[rt].ch[1];
	while (tr[u].ch[0]) u = tr[u].ch[0];
	return u;
}

inline void Delete(int x) {
	int last = Pre(x);
	int next = Next(x);
	Splay(last, 0);
	Splay(next, last);
	int del = tr[next].ch[0];
	if (tr[del].cnt > 1) {
		tr[del].cnt--;
		Splay(del, 0);
	}
	else tr[next].ch[0] = 0;
}

namespace RevolutionBP {
inline void main() {
	insert(inf);
	insert(-inf);
	read(n);
	rep (i, 1, n) {
		read(opt, x);
		switch (opt) {
			case 1: insert(x); break;
			case 2: Delete(x); break;
			case 3: write(get_rank(x), '\n'); break;
			case 4: write(kth(x + 1), '\n'); break;
			case 5: write(tr[Pre(x)].val, '\n'); break;
			case 6: write(tr[Next(x)].val, '\n'); break;
		}
	}
	return void();
}
}

signed main(){
	RevolutionBP::main();
	return 0;
}
//write: RevolutionBP
/*
7
1 106465
1 317721
1 460929
1 644985
1 84185
1 89851
6 89851
*/
posted @ 2022-03-25 22:03  RevolutionBP  阅读(47)  评论(0编辑  收藏  举报