算法与数据结构 5 - FHQ Treap

FHQ-Treap 是什么

FHQ-Treap(也叫做「分裂—合并 Treap」) 是由范浩强发明的一种实现 Treap 的方法。

原理

FHQ-Treap 通过「分裂」、「合并」操作实现快速插入、删除、查找等操作。

节点

根据 Treap 的性质,可以这样定义节点,也可以根据具体题目进行调整:

struct node {
	int ch[2]; // 儿子节点
	int val, pri; // 权值和优先级
	int cnt; // 有几个元素重复
	int siz; // 子树大小
} fhq[2000010];

节点新建、初始化和更新

这一部分比较简单,不再讲解。

int tot = 0; // 节点数量
node create(int x) {
	node res;
	res.ch[0] = res.ch[1] = 0;
	res.val = x;
	res.pri = rand();
	res.cnt = res.siz = 1;
	return res;
}
void pushup(int x) {
	fhq[x].siz = fhq[x].cnt;
	if (fhq[x].ch[0]) fhq[x].siz += fhq[fhq[x].ch[0]].siz;
	if (fhq[x].ch[1]) fhq[x].siz += fhq[fhq[x].ch[1]].siz;
}
int rt; // 总树根

分裂

分裂分为两种:按值分裂、按排名分裂。下面以按值分裂为例。
按值分裂,就是按照一个权值将 Treap 分为两棵。也就是说,需要实现这个函数:

pair<int, int> split(int cur, int k);

其中,cur 表示原 Treap 的根,k 表示将 Treap 分裂为「所有数小于等于 k」和「大于 k」两部分,以下简称为「分裂树 1」「分裂树 2」。

根据 Treap 的性质,左子树里的所有节点权值小于根,右子树里的所有节点权值大于根。因此,考虑比较当前的根权值和 k 的大小:若根权值小于等于 k,则说明根和左子树应当全部被分到「分裂树 1」中,于是向下递归,再将右子树中应当被分到「分裂树 1」的部分连接到根的右子树部分,将右子树中应当被分到「分裂树 2」的部分作为「分裂树 2」的全部(读者可在这里仔细体会);若根权值大于 k 则操作完全相反。

pair<int, int> split(int cur, int k) { // 返回两棵树的根节点编号
	if (cur == 0) return make_pair(0, 0); // Treap 为空
	if (fhq[cur].val <= k) { // 情况 1
		pair<int, int> tmp = split(fhq[cur].ch[1], k); // 递归
		fhq[cur].ch[1] = tmp.first; // 连接
		pushup(cur); // 更新
		return make_pair(cur, tmp.second);
	} else {
		pair<int, int> tmp = split(fhq[cur].ch[0], k); // 情况 2
		fhq[cur].ch[0] = tmp.second;
		pushup(cur);
		return make_pair(tmp.first, cur);
	}
}

因为每次将树分为两部分,所以复杂度为 \(O(\log n)\)

合并

「合并」指的是将原来分裂出去的两个「分裂树」重新合并。因此,待合并的两棵字数一定节点不重复,且一棵权值小于等于 k,另一颗大于 k。

假设「树甲」的权值小于「树乙」,可以考虑利用 Treap 中优先级单调的特征进行递归合并:如果「树甲」的优先级小于「树乙」,则可以将「树乙」和「树甲」的右儿子合并;否则将「树甲」和「树乙」的左儿子合并。因为每次排除一般的儿子,复杂度也为 \(O(\log n)\)

int merge(int u, int v) { // 返回合并后树的根节点编号
	if (!u && !v) return 0; // 空树
	if (u && !v) return u; // 左侧为空
	if (!u && v) return v; // 右侧为空
	if (fhq[u].pri < fhq[v].pri) {
		fhq[u].ch[1] = merge(fhq[u].ch[1], v);
		pushup(u);
		return u;
	} else {
		fhq[v].ch[0] = merge(u, fhq[v].ch[0]);
		pushup(v);
		return v;
	}
}

插入和删除

插入一个数字 \(val\) 可以分三步实现:按照 \(val\) 的大小将 Treap 分裂成「小于 \(val\)」「等于 \(val\)」「大于 \(val\)」(若不存在某部分则置为空);将 \(val\) 插入第二部分;将三部分依次合并。

删除一个数字 \(val\) 也可以分三步实现:按照 \(val\) 的大小将 Treap 分裂成「小于 \(val\)」「等于 \(val\)」「大于 \(val\)」(若不存在某部分则置为空);从第二部分删去一个 \(val\);将三部分依次合并。

因为分裂和合并的复杂度均为 \(O(\log n)\),所以插入和删除操作的复杂度也是 \(O(\log n)\)

void insert(int val) {
	pair<int, int> tmp = split(rt, val); // 将原 Treap 分裂为「小于等于」和「大于」两部分
	pair<int, int> lt = split(tmp.first, val - 1); // 将「小于等于」分裂为「小于」和「等于」两部分
	int newer;
	if (!lt.second) { // 「等于」部分不存在
		newer = ++tot; // 新开一个点
		fhq[newer] = create(val);
	} else { // 「等于」部分存在
		fhq[lt.second].cnt++; // 计次加一
		pushup(lt.second);
	}
	int ltc = merge(lt.first, !lt.second ? newer : lt.second); // 合并回去
	rt = merge(ltc, tmp.second); // 合并回去
}
void del(int val) {
	pair<int, int> tmp = split(rt, val); // 将原 Treap 分裂为「小于等于」和「大于」两部分
	pair<int, int> lt = split(tmp.first, val - 1); // 将「小于等于」分裂为「小于」和「等于」两部分
	if (fhq[lt.second].cnt > 1) { // 不止一个 val
		fhq[lt.second].cnt--; // 计次减一
		pushup(lt.second);
		lt.first = merge(lt.first, lt.second); // 第一次合并
	} else { // 只剩下一个 val:删除「等于」部分
		if (tmp.first == lt.second) {
			tmp.first = 0;
		}
		lt.second = 0;
	}
	rt = merge(lt.first, tmp.second); // 合并回去
}

根据值查询排名

查询 \(val\) 在 Treap 中的排名可以分三步实现:按照 \(val\) 的大小将 Treap 分裂成「小于 \(val\)」「大于等于 \(val\)」(若不存在某部分则置为空);查询第一部分中元素的数量;将两部分合并。

int qrank(int val) {
	pair<int, int> tmp = split(rt, val - 1); // 分裂
	int res = fhq[tmp.first].siz + 1;
	rt = merge(tmp.first, tmp.second); // 合并
	return res;
}

根据排名查询值

查询排名为 \(rk\) 的值时,直接在 Treap 上二分即可。

int qval(int cur, int rk) {
    int ls = fhq[cur].ch[0] ? fhq[fhq[cur].ch[0]].siz : 0; // 获取左儿子中的元素数量
    if (rk <= ls) return qval(fhq[cur].ch[0], rk); // 在左儿子
	else if (rk <= ls + fhq[cur].cnt) return cur; // 就是根节点
	else return qval(fhq[cur].ch[1], rk - ls - fhq[cur].cnt); // 在右儿子
}

查询前驱和后继

查询前驱和后继时,考虑将问题转化为「查询所有小于 \(val\) 的数中最大的」「查询所有大于 \(val\) 的数中最小的」,然后调用「按值分裂」和「根据排名查询值」的函数即可。

int qpre(int val) {
	pair<int, int> tmp = split(rt, val - 1);
	int ret = fhq[qval(tmp.first, fhq[tmp.first].siz)].val;
	rt = merge(tmp.first, tmp.second);
	return ret;
}
int qnex(int val) {
	pair<int, int> tmp = split(rt, val);
	int ret = fhq[qval(tmp.second, 1)].val;
	rt = merge(tmp.first, tmp.second);
	return ret;
}
posted @ 2025-03-25 23:12  cwkapn  阅读(100)  评论(0)    收藏  举报