FHQ-Treap(非旋 Treap)学习笔记

本文由 Pretharp 编写,转载需注明出处,禁止用于任何形式的商业用途。

1 引入

1.1 算法相关

FHQ-Treap 是平衡树的一种,其称为非旋 Treap(即不用旋转的 Treap),又因该数据结构由范浩强发明,所以也被称为 FHQ-Treap。本平衡是笔者认为最好写也是最简单的一种,且其全面的功能可以替代除了 Splay 以外大部分平衡树,如果不理解 Treap 中抽象的 zig 和 zag,可以先学学 FHQ-Treap。但是本篇是建立于读者有一定平衡树基础的情况下而写,如果读者完全不知道平衡树是什么可以先读读 Treap 除了 zig 和 zag 的部分。

2 算法讲解

2.1 基本性质

Treap 是 Tree(树)和 Heap(堆)的组合词汇,因此,这种数据结构有着堆的性质:对于任何一个结点,其权值大于其左儿子(若有)的权值,小于其右儿子(若有)的权值。

除此之外,你需要维护以下东西:

  • 每个结点的权值(\(val\)),子树大小(\(siz\)),左右儿子(\(ls \& rs\))编号以及其分配的一个随机权值(\(wei\))。

2.2 分裂(split)

这是 FHQ-Treap 的两个核心操作之一。分裂,就是以一个键值将平衡树分裂为两颗,通常情况下分裂后一颗子树的权值小于键值,另一颗则大于等于键值。操作具体如下:

  1. 若当前结点权值小于键值,则继续分裂其右儿子,将其右儿子小于键值的一部分作为该结点的新右儿子,反之亦然。
  2. 重复上述操作,直到当前子树为空。

代码如下:

pii split(int x, int key) {
	if(!x) {
		return {0, 0};
	}	
	if(tr[x].val < key) {
		pii t = split(tr[x].rs, key);
		tr[x].rs = t.fir, update(x); // update 用于更新结点子树大小。
		return {x, t.sec};
	} else {
		pii t = split(tr[x].ls, key);
		tr[x].ls = t.sec, update(x);
		return {t.fir, x};
	}
}

2.3 合并(merge)

另一个核心操作。为了保证树的深度一定,我们将会以每个结点的随机权值合并,代码如下:

int merge(int x, int y) {
	if(!x || !y) {
		return x + y;
	} 
	if(tr[x].wei < tr[y].wei) {
		tr[x].rs = merge(tr[x].rs, y), update(x);
		return x;
	} else {
		tr[y].ls = merge(x, tr[y].ls), update(y);
		return y;
	}
}

2.4 插入(insert)

新建一个结点,初始化即可,代码如下:

void insert(int x) {
	++ idx;
	tr[idx].siz = 1, tr[idx].val = x, tr[idx].wei = getRand(); // getRand() 用于得到一个随机数。
	pii t = split(root, x);
	/* return */ root = merge(merge(t.fir, idx), t.sec); 
}

2.5 删除(erase)

我们可以以要删除的值为键值,分裂出两颗子树,再将大于键值的子树以键值加一分裂,这样就分裂出来了三颗子树,其中权值分别小于键值、等于键值、大于键值。我们将等于键值的子树的左右儿子合并,再依次合并回来就达到了删除的效果。代码如下:

void erase(int x) {
	pii t1 = split(root, x), t2 = split(t1.sec, x + 1);
	t2.fir = merge(tr[t2.fir].ls, tr[t2.fir].rs);
	root = merge(t1.fir, merge(t2.fir, t2.sec));
}

2.6 通过权值查找排名(getRank)

以要找的权值作为键值分裂,小于键值的子树大小加一就是答案,代码如下:

int getRank(int x) {
	pii t = split(root, x);
	int res = tr[t.fir].siz + 1;
	root = merge(t.fir, t.sec);
	return res;
}

2.7 通过排名找权值(getVal)

类似于 Treap 的查找方式,流程如下:

  • 若当前结点左子树大小大于等于排名,则递归左子树。
  • 若当前结点左子树大小加一小于(也就是加上根节点的大小)排名,则在右子树中寻找第 \(rank-siz(lson)-1\) 大的结点。
  • 若当前结点左子树大小加一等于排名,则答案就是根节点。

考虑到循环写法也很好写,所以采用了常数较小的循环写法,代码如下:

int getVal(int x) {
	int p = root;
	while(p) {
		if(tr[tr[p].ls].siz + 1 == x) {
			return tr[p].val;
		} else if(tr[tr[p].ls].siz >= x) {
			p = tr[p].ls;
		} else {
			x -= tr[tr[p].ls].siz + 1;
			p = tr[p].rs;
		}
	}
}

2.8 查找前驱(lstVal)

比要找的权值的排名少一的数就是答案,代码如下:

int lstVal(int x) {
	return getVal(getRank(x) - 1);
}

2.9 查找后继(nxtVal)

考虑带需要当前值可能不止一个,按照查找前驱的方法找到的返回值可能等同于自己。不过,如果我们要寻找 \(x+1\),这个值可能不存在,所以我们找到的就会是第一个大于等于 \(x+1\) 的值,也就是后继。代码如下:

int nxtVal(int x) {
	return getVal(getRank(x + 1));
}

3 例题

3.1 例题1

例题 1:[洛谷 P6136] 【模板】普通平衡树(数据加强版)

题目大意:

你需要维护一个数据结构满足本文上述操作。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define fir first
#define sec second
const int N = 2e6 + 5;
int n, m;
namespace FHQTreap {
	#define ttFHQT FHQTreap
	const int S = (1e4 + 5e4 + 1919810) * 233 + 19;
	struct FHQTreap {
		private:
		struct Tree {
			int val, wei, ls, rs, siz;
		} tr[N];
		int idx, root = 0, seed = 1;
		void update(int x) {
			tr[x].siz = tr[tr[x].ls].siz + tr[tr[x].rs].siz + 1;
		}
		int merge(int x, int y) {
			if(!x || !y) {
				return x + y;
			}
			if(tr[x].wei < tr[y].wei) {
				tr[x].rs = merge(tr[x].rs, y), update(x);
				return x;
			} else {
				tr[y].ls = merge(x, tr[y].ls), update(y);
				return y;
			}
		}
		pii split(int x, int key) {
			if(!x) {
				return {0, 0};
			}
			if(tr[x].val < key) {
				pii t = split(tr[x].rs, key);
				tr[x].rs = t.fir, update(x);
				return {x, t.sec};
			} else {
				pii t = split(tr[x].ls, key);
				tr[x].ls = t.sec, update(x);
				return {t.fir, x};
			}
		}
		int getRand() {
			return seed *= S;
		}
		
		public:
		void insert(int x) {
			idx ++;
			tr[idx].val = x, tr[idx].siz = 1, tr[idx].wei = getRand();
			pii t = split(root, x);
			root = merge(merge(t.fir, idx), t.sec);
		}
		void erase(int x) {
			pii t1 = split(root, x), t2 = split(t1.sec, x + 1);
			t2.fir = merge(tr[t2.fir].ls, tr[t2.fir].rs);
			root = merge(t1.fir, merge(t2.fir, t2.sec));
		}
		int getRank(int x) {
			pii t = split(root, x);
			int res = tr[t.fir].siz + 1;
			root = merge(t.fir, t.sec);
			return res;
		}
		int getVal(int x) {
			int p = root;
			while(p) {
				if(tr[tr[p].ls].siz + 1 == x) {
					return tr[p].val;
				} else if(tr[tr[p].ls].siz >= x) {
					p = tr[p].ls;
				} else {
					x -= tr[tr[p].ls].siz + 1;
					p = tr[p].rs;
				}
			}
		}
		int lstVal(int x) {
			return getVal(getRank(x) - 1);
		}
		int nxtVal(int x) {
			return getVal(getRank(x + 1));
		}
	};
}
ttFHQT::ttFHQT fhq;
signed main() {
	ios_base::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1, x; i <= n; i ++) {
		cin >> x, fhq.insert(x);
	}
	int ans = 0, lst = 0;
	while(m --) {
		int op, x;
		cin >> op >> x;
		x ^= lst;
		if(op == 1) {
			fhq.insert(x);
		} else if(op == 2) {
			fhq.erase(x);
		} else if(op == 3) {
			ans ^= (lst = fhq.getRank(x));
		} else if(op == 4) {
			ans ^= (lst = fhq.getVal(x));
		} else if(op == 5) {
			ans ^= (lst = fhq.lstVal(x));
		} else {
			ans ^= (lst = fhq.nxtVal(x));
		}
	}
	return cout << ans << endl, 0;
}
posted @ 2023-10-07 22:05  Pretharp  阅读(113)  评论(0)    收藏  举报