其实谈不上学习笔记。


在谈平衡树之前,我们需要知道一种二叉树:

二叉查找树(BST)

性质

二叉查找树满足一种性质,即对于该树上的任意一个节点:

  • 该节点的权值不小于它左子树上所有节点的权值。
  • 该节点的权值不大于它右子树上所有节点的权值。

显然一颗 BST 的中序遍历就是一个权值非严格单调递增的节点序列。

在一颗 BST 上面,可以实现 插入、删除、查找排名、查找第 $k$ 大、求前驱、后继这几种操作。(此处前驱和后继的定义为最大的小于 $x$ 的值和最小的大于 $x$ 的值,下同。)

代码

关于 BST 的代码,个人认为大部分对于无旋 Treap 的学习没有什么帮助,故不提及。绝对不是因为懒。

下面是蓝书上的代码:

struct BST {
	int l, r; // 左右子节点在数组中的下标
	int val;  // 节点关键码
} a[SIZE]; // 数组模拟链表
int tot, root, INF = 1<<30;

int New(int val) {
	a[++tot].val = val;
	return tot;
}

void Build() {
	New(-INF), New(INF);
	root = 1, a[1].r = 2;
}

int Get(int p, int val) {
	if (p == 0) return 0; // 检索失败
	if (val == a[p].val) return p; // 检索成功
	return val < a[p].val ? Get(a[p].l, val) : Get(a[p].r, val);
}

void Insert(int &p, int val) {
	if (p == 0) {
		p = New(val); // 注意p是引用,其父节点的l或r值会被同时更新
		return;
	}
	if (val == a[p].val) return;
	if (val < a[p].val) Insert(a[p].l, val);
	else Insert(a[p].r, val);
}

int GetNext(int val) {
	int ans = 2; // a[2].val==INF
	int p = root;
	while (p) {
		if (val == a[p].val) { // 检索成功
			if (a[p].r > 0) { // 有右子树
				p = a[p].r;
				// 右子树上一直向左走
				while (a[p].l > 0) p = a[p].l;
				ans = p;
			}
			break;
		}
		// 每经过一个节点,都尝试更新后继
		if (a[p].val > val && a[p].val < a[ans].val) ans = p;
		p = val < a[p].val ? a[p].l : a[p].r;
	}
	return ans;
}

void Remove(int &p, int val) { // 从子树p中删除值为val的节点
	if (p == 0) return;
	if (val == a[p].val) { // 已经检索到值为val的节点
		if (a[p].l == 0) { // 没有左子树
			p = a[p].r; // 右子树代替p的位置,注意p是引用
		}
		else if (a[p].r == 0) { // 没有右子树
			p = a[p].l; // 左子树代替p的位置,注意p是引用
		}
		else { // 既有左子树又有右子树
			// 求后继节点
			int next = a[p].r;
			while (a[next].l > 0) next = a[next].l;
			// next一定没有左子树,直接删除
			Remove(a[p].r, a[next].val);
			// 令节点next代替节点p的位置
			a[next].l = a[p].l, a[next].r = a[p].r;
			p = next; // 注意p是引用
		}
		return;
	}
	if (val < a[p].val) {
		Remove(a[p].l, val);
	} else {
		Remove(a[p].r, val);
	}
}

void zig(int &p) {
	int q = a[p].l;
	a[p].l = a[q].r, a[q].r = p;
	p = q; // 注意p是引用
}

void zag(int &p) {
	int q = a[p].r;
	a[p].r = a[q].l, a[q].l = p;
	p = q; // 注意p是引用
}

(zig 和 zag 等会提及)

好了,现在你已经会平衡树了(逃

BST 的树高期望是 $\log n$ ,所以所有操作的期望复杂度为 $O(\log n)$。

看着很不错?但是如果遇到出题人稍微卡一卡:

你给我说这是二叉树?

BST 就会愉快地变成单次操作为 $O(n)$,但是我们有一些奇技淫巧可以让它变平衡

平衡树

在讲平衡树之前,我觉得需要回收一下伏笔。

左旋与右旋

还记得前面代码中的 Zig 和 Zag 吗,这就是一种方法。

Zig 是右旋,即在满足 BST 的性质下将右儿子旋转到它的父亲。

Zag 是左旋,即在满足 BST 的性质下将左儿子旋转到它的父亲。

上面就是一种普遍的平衡树的维护方式。

但是下文基本不会提到它们,因为我们写的是无旋 Treap

Treap

首先讲讲什么是 Treap。

Treap 是 Tree 和 Heap 的合成词,即用堆的性质来维护 BST。换句话说,就是这颗 BST 必须同时满足堆的性质和 BST 的性质,当 Treap 不满足堆的性质时,就需要用旋转来维护。

但是堆的权值应该是什么?首先肯定不是同一个值,因为这样等于没有优化;也不是 BST 的权值,,因为这样是自动将 BST 退化成一条链。

那么应该是什么?

我们发现 BST 在随机数据下的树高期望为 $\log n$ ,于是我们果断引入随机数来充当堆的权值,这样树高就为 $\log n$ 级别。

(上面可能有点绕。注意,BST 和堆是在同一个树上,即对于树上的每一个节点,都有两个权值,分别记录 BST 的权值和堆的权值。)

但是不用旋转,我们应该用什么来维护平衡树?

无旋 Treap(FHQ)

无旋 Treap,又称 FHQ,是一种基于分裂和合并实现的平衡树。

和带旋 Treap 一样,无旋 Treap 的节点也有两个值,分别为 BST 的权值 $v$,和堆的权值 $w$。

参量

在所有之前,我们先来清理一下对于每个节点需要哪些值:

struct sss
{
    int r,l,s,w,v;
    #define l(x) tree[x].l//左儿子
    #define r(x) tree[x].r//右儿子
    #define s(x) tree[x].s//子树大小
    #define w(x) tree[x].w//堆的权值
    #define v(x) tree[x].v//BST的权值
}tree[100005];

下面是一颗完整的普通平衡树的结构:

struct FHQ
{
    int cnt,root;//根和节点数量
    struct sss
    {
        int r,l,s,w,v;
        #define l(x) tree[x].l
        #define r(x) tree[x].r
        #define s(x) tree[x].s
        #define w(x) tree[x].w
        #define v(x) tree[x].v
    }tree[100005];
    int New(int x)
    {
        w(++cnt)=rand();
        v(cnt)=x;
        s(cnt)=1;
        return cnt;
    }
    void pushup(int x){...}
    void splitv(int root,int key,int &x,int &y){...}
    void splits(int root,int s,int &x,int &y){...}
    int merge(int x,int y){...}
    void insert(int key){...}
    void remove(int key){...}
    int v2r(int key){...}
    int r2v(int key){...}
    int pre(int key){...}
    int nxt(int key){...}
}T1;

New即新建节点。

分裂

分裂,顾名思义,是要将一颗无旋 Treap 分裂成两颗,叫做 $x$ 和 $y$,其中我们可以传进去一个值 $val$ 或者 $size$,代表将这颗无旋 Treap 以权值 $val$ 或者大小 $size$ 分界分裂成两颗无旋 Treap。其中 $x$ 的节点的权值都小于等于 $val$ 或者 左子树大小 等于 $size$。

上面的两张分裂方法被称为按值分裂按大小分裂

因为按值分裂与按大小分裂相似,所以下文只讲按值分裂。

举个例子:

首先这是一颗无旋 Treap,编号即为 $v$,默认已经满足堆性质,当前节点 $cur$ 为 $8$。

假设 $val$ 为 $5$,因为 $8\ge5$,所以节点 $8$ 及其右子树都归属于 $y$。

但是发现,左子树内也有可能有节点的 $v$ 大于 $val$,所以我们还需要继续处理递归处理左子树。

比如当前节点 $cur$ 为 $4$,因为 $4\le5$,所以 $4$ 及其左子树属于 $x$ 。

现在又要处理 $cur$ 为 $6$ 的时候了。

发现了没有?我们只需要递归处理就可以了。

下面是最终的结果:

那么我们如何将一个节点加入 $x$ 或 $y$ 呢?

我们可以用两个指针 $p$ 和 $q$ 表示遍历到 $cur$ 的时候 $x$ 和 $y$ 新加入的节点。

发现一个性质:如果 $cur$ 属于 $x$,那么 $cur$ 的左子树也属于 $x$,并且也是 $p$ 的左子树;如果 $cur$ 属于 $y$,那么 $cur$ 的右子树也属于 $y$,并且也是 $q$ 的右子树。

所以对于 $v(cur)\le val$ 时,只需要将 $cur$ 加入 $x$ 并且递归查询 $cur$ 的右子树,反之同理。

Code

void splitv(int root,int key,int &x,int &y)//按值分裂
{
    if(!root)x=y=0;
    else if(v(root)<=key)
    {
        x=root;//这里传的是引用,所以等于把root加入x中
        splitv(r(root),key,r(x),y);//root的左子树属于x,查找x的右子树
        pushup(root);
    }
    else
    {
        y=root;
        splitv(l(root),key,x,l(y));
        pushup(root);
    }
}
void splits(int root,int s,int &x,int &y)//按大小分裂
{
    if(!root)x=y=0;
    else if(s(l(root))<s)
    {
        x=root;
        splits(r(root),s-s(l(root))-1,r(x),y);
        pushup(root);
    }
    else
    {
        y=root;
        splits(l(root),s,x,l(y));
        pushup(root);
    }
}
//使用:
splitv(root,val,x,y);//返回以x和y为根的两个子树

关于上面的pushup,是这个:

void pushup(int x){s(x)=s(l(x))+s(r(x))+1;}

就是用来计算子树大小的,当然也可以维护其他的东西,比如最大子段和。

合并

把树分裂那么一定要合并回去。

为了让无旋 Treap 满足堆的性质,所以要在合并的时候动手脚。

假设我们要合并两颗无旋 Treap $x$ 和 $y$。

这里有个前提:$x$ 中所有节点的 $v$ 必须小于等于 $y$ 中最小的 $v$,否则无法保证 BST。

还是用两个指针 $p$ 和 $q$ 表示遍历 $x$ 和 $y$ 时的节点。

现在已知 $p$ 是小于 $q$ 的,那么要么把 $p$ 放在 $q$ 的左子树,要么把 $q$ 放在 $p$ 的右子树。

谁当父亲就取决于 $w$ 的值。

如果把 $p$ 放在 $q$ 的右子树,那么 $p$ 的左子树不变,把 $p$ 的右子树与 $q$ 继续合并。

反之同理。

int merge(int x,int y)
{
    if\(!x\||!y)return x^y;
    else if(w(x)>w(y))
    {
        r(x)=merge(r(x),y);
        pushup(x);
        return x;
    }
    else
    {
        l(y)=merge(x,l(y));
        pushup(y);
        return y;
    } 
}
//用法:
root=merge(x,y) //返回值即为根

插入

恭喜你已经理解了最难的那一部分。

对于插入一个值为 $key$ 的数,我们先将树以 $key$ 分裂成 $x$ 和 $y$ 两部分,这样就满足 $x\le key\le y$,就可以直接合并。

void insert(int key)
{
    int x,y;
    splitv(root,key-1,x,y);
    root=merge(merge(x,New(key)),y);
}

删除

先通过按值分裂来找出 $key$ 的位置,再通过按大小分裂找出一个 $v$ 为 $key$ 的节点。

然后合并除了这个节点剩下的两颗子树即可。

void remove(int key)
{
    int x,y,z;
    splitv(root,key-1,x,y);
    splits(y,1,y,z);
    root=merge(x,z);
}

查询排名

先按值分裂,即可得出小于 $key$ 的数的个数,最后加一即可。

int v2r(int key)
{
    int x,y,ans;
    splitv(root,key-1,x,y);
    ans=s(x)+1;
    root=merge(x,y);//别忘了合并回去
    return ans;
}

查询第 $k$ 大

按大小分裂成两颗子树 $x$ 和 $y$,那么答案一定是 $x$ 中最大的数。

那么只需要从 $x$ 的根一路向右找即可。

int r2v(int key)
{
    int x,y,ans,now;
    splits(root,key,x,y);//返回的是x和y的根
    now=x;
    while(r(now))now=r(now);
    ans=v(now);
    root=merge(x,y);
    return ans;
}

前驱

以 $key-1$ 按值分裂,得到两颗树 $x$ 和 $y$,其中 $x<key$,所以 $x$ 中最大值即为 $key$ 的前驱。

int pre(int key)
{
    int x,y,now,ans;
    splitv(root,key-1,x,y);
    now=x;
    while(r(now))now=r(now);
    ans=v(now);
    root=merge(x,y);
    return ans;
}

后继

以 $key$ 按值分裂,得到两颗树 $x$ 和 $y$,其中 $key<y$,所以 $y$ 中最小值即为 $key$ 的后继。

int nxt(int key)
{
    int x,y,now,ans;
    splitv(root,key,x,y);
    now=y;
    while(l(now))now=l(now);
    ans=v(now);
    root=merge(x,y);
    return ans;
}

现在你已经会普通平衡树了,现在来试试文艺平衡树吧!

文艺平衡树

题目

跟普通平衡树一样,只不过我们是将数的位置当成 $v$ 即可。

考虑反转操作怎么实现,发现只用交换区间中所有节点的左右儿子即可。

记得用懒标记,否则暴力下传直接 T。

输出中序遍历即可。

Code
struct FHQ
{
    int cnt,root;
    struct sss
    {
        int r,l,s,w,v,tag;
        #define l(x) tree[x].l
        #define r(x) tree[x].r
        #define s(x) tree[x].s
        #define w(x) tree[x].w
        #define v(x) tree[x].v
        #define tag(x) tree[x].tag
    }tree[100005];
    void build(){cnt=0;}
    int New(int x)
    {
        w(++cnt)=rand();
        v(cnt)=x;
        s(cnt)=1;
        return cnt;
    }
    void pushup(int x){s(x)=s(l(x))+s(r(x))+1;}
    void pushdown(int x)
    {
        if(tag(x))
        {
            swap(l(x),r(x));
            tag(l(x))^=1;
            tag(r(x))^=1;
            tag(x)=0;//发现反转后再反转等于没反转,所以使用异或
        }
    }
    void splits(int root,int s,int &x,int &y)
    {
        pushdown(root);//下传标记
        if(!root)x=y=0;
        else if(s(l(root))<s)
        {
            x=root;
            splits(r(root),s-s(l(root))-1,r(x),y);
            pushup(root);
        }
        else
        {
            y=root;
            splits(l(root),s,x,l(y));
            pushup(root);
        }
    }
    int merge(int x,int y)
    {
        if\(!x\||!y)return x^y;
        else if(w(x)>w(y))
        {
            pushdown(x);
            r(x)=merge(r(x),y);
            pushup(x);
            return x;
        }
        else
        {
            pushdown(y);
            l(y)=merge(x,l(y));
            pushup(y);
            return y;
        } 
    }
    void insert(int key)
    {
        int x,y;
        splits(root,key-1,x,y);
        root=merge(merge(x,New(key)),y);
    }
    void remove(int key)
    {
        int x,y,z;
        splits(root,key,x,z);
        splits(x,key-1,x,y);
        if(y)y=merge(l(y),r(y));
        root=merge(merge(x,y),z);
    }
    void reverse(int l,int r)
    {
        int x,y,p;
        splits(root,r,x,y);
        splits(x,l-1,x,p);//找出反转区间
        tag(p)^=1;
        root=merge(merge(x,p),y);
    }
    void print(int x)
    {
        pushdown(x);
        if(l(x))print(l(x));
        printf("%d ",v(x));
        if(r(x))print(r(x));
    }
}T1;

后记

原本是在数据结构学习笔记里面写了,但是嫌那个太潦草了,于是有了这个。

可持久化平衡树还没学,有空再补。