平衡树

\(Write-by-尹以豪\)

本篇文章主要介绍 \(\sf Treap\) 以及 \(\Huge fhq-Treap\)

PS* :本篇文章只是堆 \(\sf Treap\) 做大概的讲解,着重分析 \(\Huge fhq-Treap\)

\(\sf Treap\) 的简介

顾名思义 \(\boxed{ Treap = Tree + Heap}\) ,也就是 \(\sf Treap\) 同时满足 \(Tree\) (二叉搜索树) 的优美性质,也满足 \(Heap\) (堆) 的性质

专业的,\(\sf Treap\) 是一种 弱平衡二叉搜索树

二叉搜索树只需要了解其性质吧

二叉搜索树有着优美的性质,我们对二叉搜索树经行操作 (插入,删除,找最大,找最小) ,所需的时间复杂度是 \(O(h)\) ,也就是树的高度,但有时候树的形态是一条链的话 (就比如下面这一个)

image

这样的话时间复杂度就直接变成 \(\color{red}{O(n)}\)\(\tiny TAT\) ,那我们再 \(q\) 次操作 \((1 \leq q \leq 10^5)\) ,你不就炸了吗,而且不难发现,假设这一条链只有 \(3\) 个节点,我们可以令 \(2\) 为这一颗树的根节点, \(1\)\(2\) 的左儿子, \(3\)\(2\) 的右儿子,如下。

image

这样的时间复杂度就会缩小,如果有 \(10^5\) 个节点呢?我们也可以对其经行转化,单次处理的时间复杂度就由 \(O(n)\) 变为 \(O(\log n)\) 了!怎么转化呢,这就要我们的 \(\sf Treap\) 了。

\(\sf Treap\) 的各种操作

首先先讲讲 \(\sf Treap\) 是怎么维护他的「平衡」,其实特别抽象巧妙,就是随机数说出来可能不信 ,我第一次了解到甚至觉得是玄学。

言归正传,\(\sf Treap\) 每一个节点不仅有一个权值\(\small val\))也通过给每一个节点随机分配了一个优先级(\(\small priority\))。

其中,权值\(\small val\))用来维护二叉搜索树的性质,也就是如下:

  • 对于节点 \(x\) ,左子树全部节点的权值(\(\small val\))均比 \(x\) 的权值(\(\small val\))小。
  • 对于节点 \(x\) ,右子树全部节点的权值(\(\small val\))均比 \(x\) 的权值(\(\small val\))大。

优先级\(\small priority\))维护的是堆的性质:

  • 比父亲节点大或者小 (具体看是大根堆还是小根堆)

那我们为什么需要大费周章的去让这个数据结构符合树和堆的性质,并且随机给出堆的值呢?

要理解这个,首先需要理解朴素二叉搜索树的问题。在给朴素搜索树插入一个新节点时,我们需要从这个搜索树的根节点开始递归,如果新节点比当前节点小,那就向左递归,反之亦然。

最后当发现当前节点没有子节点时,就根据新节点的值的大小,让新节点成为当前节点的左或右子节点。

如果插入结点的权值是随机的(换言之,是随机插入的),那这个朴素搜索树的高度较小(接近 \(\log n\) ,其中 \(n\) 为结点数),而每一层的节点数较多,即它的形状会非常的「平衡」。\(\sf Treap\) 就是一个例子。因此此时的任意操作复杂度都将会是 \(O(\log n)\) 左右。

\(\sf Treap\) 解决了退化成链、达到一个较为「平衡」的状态,通过维护随机的优先级满足堆性质,「打乱」了节点的插入顺序,从而让二叉搜索树达到了理想的复杂度,避免了退化成链的问题。

旋转 ,就是以 \(O(1)\) 的时间复杂度将一棵子树的根节点转换为根节点的儿子节点,来维护「平衡」,不做讲解。

还有许多的操作,但这里不做详细的讲解

很难理解对吧,那不如看看fhq-Treap,很好理解的awa

\(\Huge fhq-Treap\)

\(fhq-Treap\) 是平衡树的一种,它不用 旋转 等操作来维护树的平衡,而且只有两个主要操作,分别是 分裂(split)合并(merge)

无旋 treap 的操作方式使得它天生支持维护序列、可持久化等特性。

分裂(split)操作

分裂操作接受两个参数:根指针 \(\small root\) 、关键值 \(\small key\) 。分裂操作结果是将以 \(\small root\) 为根节点的子树分裂成两个 \(\sf treap\) ,第一个 \(\sf treap\) 的所有节点的值都 \(\leq \small key\) ,第二个 \(\sf treap\) 的所有节点的值 \(> \small key\)

image

我觉得是十分好理解的,那就直接上代码吧。

void split(int now,int val, int &x, int &y)
{
    if(!now) x = y = 0;
    else 
    {
        if(fhq[now].val <= val) // 当前正在考虑的节点(根) <= val ,now 以及他的左子树全部都属于分裂后的第一个子树
        {
            x = now;
            split(fhq[now].r, val, fhq[now].r, y);
        }
        else // 反之,则 now 及他的右子树全部都属于分裂后的第二个子树 
        {
            y = now;
            split(fhq[now].l, val, x, fhq[now].l);
        }
        update(now); // 相当于线段树中的 pushup
    }
}

补充一下,查询一个数的排名可以这样写:

void getrank(int val)
{
    split(root, val - 1, x, y);
    printf("%d\n", fhq[x].size + 1);
    root = merge(x, y);
}
void getnum(int rank)
{
    int now = root;
    while(now)
    {
        if(fhq[fhq[now].l].size + 1 == rank)
            break;
        else if(fhq[fhq[now].l].size >= rank)
            now = fhq[now].l;
        else 
        {
            rank -= fhq[fhq[now].l].size + 1;
            now = fhq[now].r;
        }
    }
    printf("%d\n", fhq[now].val);
}

合并操作(merge)

合并过程接受两个参数:左 \(\sf treap\) 的根指针 \(\textit{u}\) 、右 \(\sf treap\) 的根指针 \(\textit{v}\) 。必须满足 \(\textit{u}\) 中所有结点的值小于等于 \(\textit{v}\) 中所有结点的值。一般来说,我们合并的两个 \(\sf treap\) 都是原来从一个 \(\sf treap\) 中分裂出去的,所以不难满足 \(\textit{u}\) 中所有节点的值都小于 \(\textit{v}\)

前面的 \(\sf Treap\) ,是通过 旋转操作 来维护 \(\small priority\) 的堆性质,同时还能维护二叉搜索树本身的性质,在 $ fhq-Treap$ 中,我们通过 合并操作 来达成同样的效果。

因为我们要合并的两个 \(\sf treap\) 都已经有序了,所以只需要根据 \(\small proirity\) 的数据来维护堆的性质,其他的具体见代码。

\(\scriptscriptstyle 打的是真累啊,不如直接把代码甩给你们qwq,干巴巴地陈述是真难搞!>A<\)

// 维护大根堆的 treap
int merge(int x, int y) // 返回的是一个值 (当前这个子树那个是根节点)
{
    if(!x or !y) return x + y;
    if(fhq[x].key > fhq[y].key) // x 的值比 y 的大,所以以 x 为根
    {
        fhq[x].r = merge(fhq[x].r,y);
        update(x); // 相当于 pushup
        return x;
    }
    else // 反之则就以 y 为根
    {
        fhq[y].l = merge(x, fhq[y].l);
        update(y); // 相当于 pushup
        return y;
    }
}

插入操作(insert)

看了 wiki 感觉好困难,我就给个代码附上注释。

void insert(int val)
{
    split(root, val, x, y);
    root = merge(merge(x, newnode(val)), y);
}

例题 P3369【模板】普通平衡树

我就给出完整代码了,以上没有补充的可以在下面补充了\(\scriptscriptstyle 吧\)

#include<bits/stdc++.h>
#include<random>
#define LL long long

const int maxn = 1e5 + 5;
struct Node
{
    int l, r;
    int val, key;
    int size;
}fhq[maxn];

int cnt, root;
std::mt19937 rnd(233);
int newnode(int val)
{
    fhq[++cnt].val = val;
    fhq[cnt].key = rnd();
    fhq[cnt].size = 1;
    return cnt;
}
void update(int now)
{
    fhq[now].size = fhq[fhq[now].l].size + fhq[fhq[now].r].size + 1;
}
void split(int now,int val, int &x, int &y)
{
    if(!now) x = y = 0;
    else 
    {
        if(fhq[now].val <= val)
        {
            x = now;
            split(fhq[now].r, val, fhq[now].r, y);
        }
        else 
        {
            y = now;
            split(fhq[now].l, val, x, fhq[now].l);
        }
        update(now);
    }
}
int merge(int x,int y)
{
    if(!x or !y) return x + y;
    if(fhq[x].key > fhq[y].key)           // > >= < <=
    {
        fhq[x].r = merge(fhq[x].r,y);
        update(x);
        return x;
    }
    else 
    {
        fhq[y].l = merge(x, fhq[y].l);
        update(y);
        return y;
    }
}
int x, y, z;
void ins(int val)
{
    split(root, val, x, y);
    root = merge(merge(x, newnode(val)), y);
}
void del(int val)
{
    split(root, val, x, z);
    split(x, val - 1, x, y);
    y = merge(fhq[y].l, fhq[y].r);
    root = merge(merge(x, y), z);
}
void getrank(int val)
{
    split(root, val - 1, x, y);
    printf("%d\n", fhq[x].size+1);
    root = merge(x, y);
}
void getnum(int rank)
{
    int now = root;
    while(now)
    {
        if(fhq[fhq[now].l].size + 1 == rank)
            break;
        else if(fhq[fhq[now].l].size >= rank)
            now = fhq[now].l;
        else 
        {
            rank -= fhq[fhq[now].l].size + 1;
            now = fhq[now].r;
        }
    }
    printf("%d\n", fhq[now].val);
}
void pre(int val)
{
    split(root, val - 1, x, y);
    int now = x;
    while(fhq[now].r)
        now = fhq[now].r;
    printf("%d\n", fhq[now].val);
    root=merge(x,y);
}
void nxt(int val)
{
    split(root,val,x,y);
    int now = y;
    while(fhq[now].l)
        now = fhq[now].l;
    printf("%d\n", fhq[now].val);
    root = merge(x, y);
}
int main()
{
    int t;
	scanf("%d", &t);
    while(t--)
    {
        int opt, x;
		scanf("%d%d", &opt, &x);
        switch(opt)
        {
            case 1:
                ins(x);
                break;
            case 2:
                del(x);
                break;
            case 3:
                getrank(x);
                break;
            case 4:
                getnum(x);
                break;
            case 5:
                pre(x);
                break;
            case 6:
                nxt(x);
                break;
        }
    }
    return 0;
}

练习题

P5338 [TJOI2019] 甲苯先生的滚榜

点击查看代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int n, m, root;
int tot, ans, s[100005], ls;
struct st
{
	int ria, rib;
	bool operator < (const st b) const
	{
		if(ria == b.ria) return rib < b.rib;
		else return ria > b.ria;
	}
	bool operator <= (const st b) const
	{
		if(ria == b.ria) return rib <= b.rib;
		else return ria >= b.ria;
	}
}a[100005];
struct ss
{
	int lson, rson, rnd, sz, cnt;
	st val;
}e[200005];
void push_up(int x)
{
	e[x].sz = e[e[x].lson].sz + e[e[x].rson].sz + e[x].cnt;
}
int merge(int x,int y)
{
	if(!x || !y) return x | y;
	if(e[x].rnd < e[y].rnd)
	{
		e[x].rson = merge(e[x].rson, y);
		push_up(x);
		return x;
	}
	else
	{
		e[y].lson = merge(x,e[y].lson);
		push_up(y);
		return y;
	}
}
void split(st k, int now, int &x, int &y)
{
	if(!now)
	{
		x = y = 0;
	}
	else
	{
		if(e[now].val <= k)
		{
			x = now;
			split(k, e[now].rson, e[now].rson, y);
		}
		else
		{
			y = now;
			split(k, e[now].lson, x, e[now].lson);
		}
	}
	push_up(now);
}
int new_node(st k)
{
	int now;
	if(ls) now = s[ls--];
	else now = ++tot;
	e[now].sz = e[now].cnt = 1;
	e[now].val = k;
	e[now].rnd = rand();
	e[now].lson = e[now].rson = 0;
	return now;
}
void insert(st x)
{
	int l, r;
	split(x, root, l, r);
	l = merge(l, new_node(x));
	root = merge(l, r);
}
void del(st x)
{
	int l, a, r;
	st y = x;
	y.rib--;
	split(x, root, l, r);
	split(y, l, l, a);
	s[++ls] = a;
	a=merge(e[a].lson, e[a].rson);
	root = merge(merge(l, a), r);
}
int ask_val(st x)
{
	int l, r;
	x.rib--;
	split(x, root, l, r);
	int re = e[l].sz + 1;
	root = merge(l, r);
	return re - 1;
}
typedef unsigned int ui;
    ui randNum(ui& seed, ui last, const ui m) {
    seed = seed * 17 + last;
    return seed % m + 1;
}
ui seed, last = 7;
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int t;
	cin >> t;
	while(t--)
	{
		srand(19260817);
		cin >> m >> n >> seed;
		root = ls = tot = 0;
		for(int i = 1; i <= m; i++)
			a[i].ria = a[i].rib = 0, insert(a[i]);
		int u, v;
		for(int i = 1; i <= n; i++)
		{
			u = randNum(seed, last, m);
			v = randNum(seed, last, m);
			del(a[u]);
			a[u].ria++;
			a[u].rib += v;
			insert(a[u]);
			last = ask_val(a[u]);
			cout << last << '\n';
		}
	}
	return 0;
}

P2596 [ZJOI2006] 书架

点击查看代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int maxn = 8e4 + 5;

int n, m, id[maxn], a[maxn], root, r1, r2, r3, r4, cnt = 0;

struct treap{
    int ch[2], fa, size, rd, val;
}t[maxn];

int newnode(int val)
{
    t[++cnt].val = val;
	t[cnt].rd = rand();
	t[cnt].size = 1;
    id[val] = cnt;
	return cnt;
}

void pushup(int x)
{
	t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;
}

void split(int x, int k, int &a, int &b, int faa = 0, int fab = 0)
{
    if(x == 0)
	{
		a = b = 0;
		return;
	}
    if(k <= t[t[x].ch[0]].size)
	{
		t[x].fa = fab;
		b = x;
		split(t[x].ch[0], k, a, t[x].ch[0], faa, x);
	}
    else
	{
		t[x].fa = faa;
		a = x;
		split(t[x].ch[1], k - t[t[x].ch[0]].size - 1, t[x].ch[1], b, x, fab);
	}
	pushup(x);
}

int merge(int x, int y)
{
    if(x == 0 || y == 0) return x + y;
    if(t[x].rd < t[y].rd)
	{
		t[x].ch[1] = merge(t[x].ch[1], y);
		t[t[x].ch[1]].fa = x;
		pushup(x);
		return x;
    }
    else
	{
		t[y].ch[0] = merge(x, t[y].ch[0]);
		t[t[y].ch[0]].fa = y;
		pushup(y);
		return y;
    }
}

void insert(int pos, int val)
{
    split(root, pos, r1, r2);
    root = merge(r1, merge(newnode(val), r2));
}

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

int find(int cnt)
{
    int node = cnt, res = t[t[cnt].ch[0]].size + 1;
    while(node != root and cnt)
	{
		if(get(cnt)) res += t[t[t[cnt].fa].ch[0]].size + 1;
		cnt = t[cnt].fa;
    }
    return res;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
    char opt[10];
	int x, y, k;
	cin >> n >> m; srand(19260817);
    for(int i = 1; i <= n; i++)
		cin >> a[i], insert(i - 1, a[i]);
    for(int i = 1; i <= m; i++)
	{
		cin >> opt >> x;
		if(opt[0] == 'T')
		{
		    k = find(id[x]);
		    split(root, k, r1, r3);
		    split(r1, k - 1, r1, r2);
		    root = merge(r2, merge(r1, r3));
		}
		if(opt[0] == 'B')
		{
		    k = find(id[x]);
		    split(root, k, r1, r3, 0);
		    split(r1, k - 1, r1, r2, 0);
		    root = merge(r1, merge(r3, r2));
		}
		
		if(opt[0] == 'I')
		{
		    cin >> y;
			k = find(id[x]);
		    if(y)
			{
				if(y > 0)
				{
				    split(root, k + 1, r3, r4);
				    split(r3, k, r2, r3);
				    split(r2, k - 1, r1, r2);
				    root = merge(r1, merge(r3, merge(r2, r4)));
				}
				else
				{
				    split(root, k, r3, r4);
				    split(r3, k - 1, r2, r3);
				    split(r2, k - 2, r1, r2);
				    root = merge(r1, merge(r3, merge(r2, r4)));
				}
		    }
		}
		
		if(opt[0] == 'A')
		{
		    k = find(id[x]);
		    cout << k - 1 << '\n';
		}
		
		if(opt[0] == 'Q')
		{
		    split(root, x, r1, r2);
		    int node = r1;
		    while(t[node].ch[1]) node = t[node].ch[1];
		    cout << t[node].val << '\n';
		    root = merge(r1, r2);
		}
    }
    return 0;
}

P3391 【模板】文艺平衡树

点击查看代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;

struct Node
{
	Node* ch[2];
	int val, prio;
	int cnt;
	int siz;
	bool to_rev = false;
	
	Node(int _val) : val(_val), cnt(1), siz(1)
	{
		ch[0] = ch[1] = nullptr;
		prio = rand();
	}
	
	int upd_siz()
	{
		siz = cnt;
		if(ch[0] != nullptr) siz += ch[0] -> siz;
		if(ch[1] != nullptr) siz += ch[1] -> siz;
		return siz;
	}
	
	void pushdown()
	{
		swap(ch[0], ch[1]);
		if(ch[0] != nullptr) ch[0] -> to_rev ^= 1;
		if(ch[1] != nullptr) ch[1] -> to_rev ^= 1;
		to_rev = false;
	}
	
	void check_tag()
	{
		if(to_rev)
			pushdown();
	}
};

struct Seg_treap
{
	Node* root;
	#define siz(_) (_ == nullptr ? 0 : _ -> siz)

  	pair<Node*, Node*> split(Node* cur, int sz)
	{
	    if(cur == nullptr) return {nullptr, nullptr};
	    cur -> check_tag();
	    if(sz <= siz(cur -> ch[0]))
		{
			auto temp = split(cur -> ch[0], sz);
			cur -> ch[0] = temp.second;
			cur -> upd_siz();
			return {temp.first, cur};
	    }
		else
		{
			auto temp = split(cur -> ch[1], sz - siz(cur -> ch[0]) - 1);
			cur -> ch[1] = temp.first;
			cur -> upd_siz();
			return {cur, temp.second};
	    }
	}

	Node* merge(Node* sm, Node* bg)
	{
		if(sm == nullptr and bg == nullptr) return nullptr;
		if(sm != nullptr and bg == nullptr) return sm;
		if(sm == nullptr and bg != nullptr) return bg;
		sm -> check_tag(), bg -> check_tag();
		if(sm -> prio < bg -> prio)
		{
			sm -> ch[1] = merge(sm -> ch[1], bg);
			sm -> upd_siz();
			return sm;
		}
		else
		{
			bg -> ch[0] = merge(sm, bg -> ch[0]);
			bg -> upd_siz();
			return bg;
		}
	}
	
	void insert(int val)
	{
		auto temp = split(root, val);
		auto l_tr = split(temp.first, val - 1);
		Node* new_node;
		if(l_tr.second == nullptr) new_node = new Node(val);
		Node* l_tr_combined = merge(l_tr.first, l_tr.second == nullptr ? new_node : l_tr.second);
		root = merge(l_tr_combined, temp.second);
	}
	void seg_rev(int l, int r)
	{
		pair<Node*, Node*> less = split(root, l - 1);
		pair<Node*, Node*> more = split(less.second, r - l + 1);
		more.first -> to_rev = true;
		root = merge(less.first, merge(more.first, more.second));
	}
	
	void print(Node* cur)
	{
		if(cur == nullptr) return;
		cur -> check_tag();
		print(cur -> ch[0]);
		cout << cur -> val << ' ';
		print(cur -> ch[1]);
	}
};

Seg_treap tr;

int main()
{
	srand(time(nullptr));
	int n, m;
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		tr.insert(i);
	while(m--)
	{
		int l, r;
		cin >> l >> r;
		tr.seg_rev(l, r);
	}
	tr.print(tr.root);
	return 0;
}

Ending

最上面的两幅图是我在机房用 Python 的第三方库 Manim 画的 (骄傲),代码如下 (图2).

点击查看代码
from manim import *


class TreeNode:
    def __init__(self, value, x=0, y=0, radius=0.5):
        self.value = value
        self.left = None
        self.right = None
        self.x = x
        self.y = y
        self.radius = radius
        self.circle = Circle(radius=radius, fill_opacity=1, color=BLUE_C).move_to([x, y, 0])
        self.text = Text(str(value)).move_to([x, y, 0])
        self.group = VGroup(self.circle, self.text)


class LinkedTree(Scene):
    def construct(self):
        root = TreeNode(2, x=0, y=1)
        self.play(Create(root.group))
        self.wait(0.5)

        node1 = TreeNode(1, x=-2, y=-1)
        self.add_edge(root, node1)
        self.play(Create(node1.group))
        self.wait(0.5)

        node3 = TreeNode(3, x=2, y=-1)
        self.add_edge(root, node3)
        self.play(Create(node3.group))
        self.wait(0.5)




    def add_edge(self, parent, child):
        edge = Line(
            start=[parent.x, parent.y - parent.radius, 0],
            end=[child.x, child.y + child.radius, 0],
            color=WHITE
        )
        self.play(Create(edge))
        setattr(child, 'edge', edge)
posted @ 2025-07-15 08:44  StudentE  阅读(21)  评论(1)    收藏  举报