【笔记 & 题解 | FHQ Treap(可持久化)】洛谷P3369【模板】普通平衡树 & 洛谷 P3835 【模板】可持久化平衡树

回归数据结构了,第一个想起幼时噩梦平衡树。

题面:

我们需要一种数据结构,

支持插入、删除、按位查询、查询前驱后继、查询比固定值小的值数量等操作。

平衡树:树上任一结点的左子树和右子树的高度之差不超过 1,两个子树“平衡”的树结构。

在平衡树上执行一次操作,时间复杂度一般为 O(log \ n)

不“平衡”的平衡树一次操作会退化为 O(n)

解析:

我个人推荐两种方法,初学者使用 FHQ Treap,可了解算法底层逻辑。

pbds 码量很小,并且 noi 系列比赛明确可以使用,也有必要学。

(这篇先讲 FHQ,pbds 的代码放在这里

其它的不是比 Treap 码量大,就是不如这俩快,可以不学。

(1)FHQ Treap

FHQ Treap(无旋 Treap)是由清华大学的范浩强提出的一种平衡树实现方式。

相较于 Splay(伸展树),它不使用旋转操作来维持平衡。

(可以理解 Splay 是有点被淘汰的平衡树算法,有多而复杂的旋转操作。

   它通过每次询问都将被询问点不断“伸展”,向上旋转至根节点。

   来保持二叉查找树性质,实现自适应性,维持均摊平衡。

但在动态树算法方面,Splay 是不可或缺的,我先挖个坑以后讲)

而是通过两个核心操作:split(分割)和 merge(合并)来实现所有功能。

Treap 节点结构体定义:

Treap 是 Tree 和 Heap 的组合,既满足二叉搜索树性质也满足堆性质

二叉搜索树性质: 左子树中所有节点的值都小于该节点的值,

                              右子树中所有节点的值都大于该节点的值,左右子树也都是二叉搜索树

(最大)堆性质:对于树中任意节点,其值都大于或等于其子节点的值,

                             且根节点是树中的最大值。

#define lc(p) tr[p].ls  //方便调用左右节点的宏定义,我个人码风,可以不加 
#define rc(p) tr[p].rs
const int N = 1e5 + 10; 

struct node {
    int ls, rs;  // 左右子节点
    int val;    // 节点值:用于维护二叉搜索树性质,保证树的平衡,分割操作的时候会用到
    int siz;    // 以该节点为根的子树大小
    int rnd;    // 随机优先级:用于维护堆性质,保证树的平衡,合并操作的时候会用到
} tr[N]; 

int rt, trlen;  // rt为根节点,trlen为已分配节点数

普通操作函数:
// 创建新节点:初始化
int newd(int v) {
	trlen++; 
    tr[trlen] = {0, 0, v, 1, rand()};  //子树大小为 1,随机优先级
    return trlen; 
}

// 更新节点p的子树大小
void pushup(int p) { 
    tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1; 
}

关于优先级为什么是随机的这回事:

如果你按顺序插入序列 \left \{ 1,2,3,4,5,6 \right \},按二叉搜索树性质,就会变成这样:

这是一条链,查找起来是 O(n) 的,也就是不平衡

但如果我们加上随机优先级,假设优先级对应是这样的:\left \{ 1,2,3,4,5,6 \right \}\Rightarrow (7,5,8,2,6,8)

那么就有如下流程(更具体的合并操作请看 merge 部分代码):

(图片来源:董晓算法)

可以达成平衡。

split 操作(分割):

Treap 的一个核心操作。

将一棵树按 val 值分割成两棵树,分割操作完成后 x 是左子树,y 是右子树

并且 lc(p) 所有的值 <= tr[p].val <= rc(p) 所有的值。 

递归过程中,有:

x(是所有节点值 ≤ val)的子树的根

y(是所有节点值 > val)的子树的根

代码:

// p是要被分割的子树根,按 v 值分割,x 和 y 一开始传进来是啥也没有的,需要在函数中一次次更新 
void split(int p, int v, int &x, int &y) {
    if (p == 0) {  // 空树情况
		x = y = 0;
		return;
	}
    if (tr[p].val <= v) { //说明 lc(p) 可以整个归到左子树 
        x = p;    //先把左子树定位 p,递归完成回溯时 p里面的值都小于等于 v 
        split(rc(p), v, rc(x), y);  
		//接下来分割以 rc(p) 为根的子树,因为 lc(p)所有的值 <= v,但是 rc(p) 只有一部分 <=v 
    }
    else {  //说明 rc(p) 可以整个归到右子树 
        y = p;    //先把右子树定位 p,递归完成回溯时 p里面的值都大于 v 
        split(lc(p), v, x, lc(y));
		//接下来分割以 lc(p) 为根的子树,因为 rc(p)所有的值 > v,但是 lc(p) 只有一部分 >v 
    }
    pushup(p);  // 更新 p 节点信息
}

merge 操作(合并):

这是 Treap 的另一个核心操作,与 split 互为逆操作。

合并两棵树 x 和 y,因为代码实现中都是 split 后面紧跟着 merge

就能保证 x 中所有节点值 <= y 中所有节点值。 

代码:

int merge(int x, int y) {
    if ( (!x) || (!y) ) return x + y;   //空树情况,当 x和 y其中一方为 0,那输出另一方 
    // 优先级 rnd 较小的节点作为根,保证树的平衡
    if (tr[x].rnd < tr[y].rnd) {   
        rc(x) = merge(rc(x), y);  // 将 y合并到 x的右子树
        pushup(x);
        return x;
    }
    else {
        lc(y) = merge(x, lc(y));  // 将 x合并到 y的左子树
        pushup(y);
        return y;
    }
}

实现操作函数:

插入与删除:

// 插入值 v
void ins(int v) {
    int x, y;
    split(rt, v, x, y);  // 将树按 v 分割成 x(y<=v) 和 y(y>v)
    rt = merge( merge( x, newd(v) ), y );  // 在 x 和 y 之间插入新节点后合并
    //你愿意的话这么写也没问题:rt = merge( x, merge( newd(v), y ) ); 
}

// 删除值 v(只删除一个匹配项)
void del(int v)
{
    int x, y, z;
    split(rt, v-1, x, y);  // 分割出 x(<=v-1) 和 y(>=v)
    split(y, v, y, z);     // 将 y 分割成 y(=v) 和 z(>v)
    //合并 y 的左右子树(即删除 y 的根节点),然后与 x 和 z 合并
    rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}

获取排名:

//获取值 v 的排名(即 v 是第几小的元素)
int getrnk(int v) {
    int x, y;
    split(rt, v-1, x, y);  // 分割出 x(<=v-1) 和 y(>=v),因为有多个重复值只能这么做 
    int res = tr[x].siz+1;  // x 的大小 + 1 即为 v 的排名
    rt = merge(x, y);  //合并(恢复)原树
    return res;
}

// 获取排名为 k 的元素值
int getval(int p, int k) {
    if ( k == tr[lc(p)].siz + 1 ) {
		return tr[p].val;  // 正好是当前节点
	}
    if ( k <= tr[lc(p)].siz ) {
		return getval(lc(p), k);  // 在左子树中
	}
    else {
		return getval(rc(p), k - tr[lc(p)].siz - 1);  // 在右子树中
	}
}

前驱后继:

// 获取v的前驱(小于 v 的最大值)
int getpre(int v) {
    int x, y;
    split(rt, v - 1, x, y);  // 分割出 x(<=v-1) 和 y(>=v)
    int res = getval(x, tr[x].siz);  // 获取 x 中排名为 tr[x].siz 的元素(即最大值)
    rt = merge(x, y);        // 恢复原树
    return res;
}

// 获取v的后继(大于v的最小值)
int getnxt(int v) {
    int x, y;
    split(rt, v, x, y);      // 分割出 x(<=v)和 y(>v)
    int res = getval(y, 1);  // 获取 y 中排名为 1 的元素(即最小值)
    rt = merge(x, y);        // 恢复原树
    return res;
}

洛谷 P3369 代码

主函数就不单独放了,直接上整代码(无注释,方便调式):

#include<bits/stdc++.h>
using namespace std;

#define lc(p) tr[p].ls
#define rc(p) tr[p].rs

const int N = 1e5 + 10;

struct node {
	int ls, rs;
	int val;
	int siz;
	int rnd;
} tr[N];

int rt, trlen;

int newd(int v) {
	trlen++;
	tr[trlen] = {0, 0, v, 1, rand()};
	return trlen;
}

void pushup(int p) {
	tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1;
}

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

int merge(int x, int y) {
	if ( (!x) || (!y) ) {
		return x + y;
	}
	if (tr[x].rnd < tr[y].rnd) {
		rc(x) = merge(rc(x), y);
		pushup(x);
		return x;
	}
	else {
		lc(y) = merge(x, lc(y));
		pushup(y);
		return y;
	}
}

void ins(int v) {
	int x, y;
	split(rt, v, x, y);
	rt = merge( merge( x, newd(v) ), y );
}

void del(int v) {
	int x, y, z;
	split(rt, v - 1, x, y);
	split(y, v, y, z);
	rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}

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

int getval(int p, int k) {
	if (tr[lc(p)].siz + 1 == k) {
		return tr[p].val;
	}
	if (k <= tr[lc(p)].siz) {
		return getval(lc(p), k);
	}
	else {
		return getval(rc(p), k - tr[lc(p)].siz - 1);
	}
}

int getpre(int v) {
	int x, y;
	split(rt, v - 1, x, y);
	int res = getval(x, tr[x].siz);
	rt = merge(x, y);
	return res;
}

int getnxt(int v) {
	int x, y;
	split(rt, v, x, y);
	int res = getval(y, 1);
	rt = merge(x, y);
	return res;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n;
	cin >> n;
	rt = trlen = 0;
	
	for (int i = 1; i <= n; i++) {
		int opt, x;
		cin >> opt >> x;
		
		if (opt == 1) {
			ins(x);
		}
		else if (opt == 2) {
			del(x);
		}
		else if (opt == 3) {
			cout << getrnk(x) << "\n";
		}
		else if (opt == 4) {
			cout << getval(rt, x) << "\n";
		}
		else if (opt == 5) {
			cout << getpre(x) << "\n";
		}
		else if (opt == 6) {
			cout << getnxt(x) << "\n";
		}
	}
	
	return 0;
}

(2)可持久化 FHQ Treap

可持久化数据结构:是指能够保留历史版本的数据结构。

                                每次修改操作后,原版本仍然可用,新操作基于原版本创建新版本。

并且每次开新节点只会记录与前一个版本不一样的结点,也就是动态开点。

强烈推荐董晓老师的这个视频!!!看完保证全懂!!

我这里就只放代码。

洛谷 P3835 代码:
#include<bits/stdc++.h>
using namespace std;

#define lc(p) tr[p].ls
#define rc(p) tr[p].rs

const int N = 5e5 + 10;

struct node {
	int ls, rs;
	int val;
	int siz;
	int rnd;
} tr[N * 50];

int root[N], trlen;

int newd(int v) {
	trlen++;
	tr[trlen] = {0, 0, v, 1, rand()};
	return trlen;
}

void pushup(int p) {
	tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1;
}

void split(int p, int v, int &x, int &y) {
	if (p == 0) {
		x = y = 0;
		return ;
	}
	if (tr[p].val <= v) {
		trlen++;
		x = trlen;   //这里一定不能这么写:x = trlen++; 因为要先自增再赋值 
		tr[x] = tr[p];
		split(rc(p), v, rc(x), y);
		pushup(x);
	}
	else {
		trlen++;
		y = trlen;
		tr[y] = tr[p];
		split(lc(p), v, x, lc(y));
		pushup(y);
	}
}

int merge(int x, int y) {
	if ( (!x) || (!y) ) {
		return x + y;
	}
	if (tr[x].rnd < tr[y].rnd) {
		rc(x) = merge(rc(x), y);
		pushup(x);
		return x;
	}
	else {
		lc(y) = merge(x, lc(y));
		pushup(y);
		return y;
	}
}

void ins(int &rt, int v) {
	int x, y;
	split(rt, v, x, y);
	rt = merge( merge( x, newd(v) ), y );
}

void del(int &rt, int v) {
	int x, y, z;
	split(rt, v - 1, x, y);
	split(y, v, y, z);
	rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}

int getrnk(int &rt, int v) {
	int x, y;
	split(rt, v - 1, x, y);
	int res = tr[x].siz + 1;
	rt = merge(x, y);
	return res;
}

int getval(int p, int k) {
	if (tr[lc(p)].siz + 1 == k) {
		return tr[p].val;
	}
	if (k <= tr[lc(p)].siz) {
		return getval(lc(p), k);
	}
	else {
		return getval(rc(p), k - tr[lc(p)].siz - 1);
	}
}

int getpre(int &rt, int v) {
	int x, y;
	split(rt, v - 1, x, y);
  	if (!x) {
	  	return -2147483647;
	}
	
	int res = getval(x, tr[x].siz);
	rt = merge(x, y);
	return res;
}

int getnxt(int &rt, int v) {
	int x, y;
	split(rt, v, x, y);
  	if (!y) {
	  	return 2147483647;
	}
	
	int res = getval(y, 1);
	rt = merge(x, y);
	return res;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n;
	cin >> n;
	root[0] = trlen = 0;
	
	for (int i = 1; i <= n; i++) {
		int v, opt, x;
		cin >> v >> opt >> x;
		root[i] = root[v];
		
		if (opt == 1) {
			ins(root[i], x);
		}
		else if (opt == 2) {
			del(root[i], x);
		}
		else if (opt == 3) {
			cout << getrnk(root[i], x) << "\n";
		}
		else if (opt == 4) {
			cout << getval(root[i], x) << "\n";
		}
		else if (opt == 5) {
			cout << getpre(root[i], x) << "\n";
		}
		else if (opt == 6) {
			cout << getnxt(root[i], x) << "\n";
		}
	}
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值