算法与数据结构 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;
}
本文来自博客园,作者:cwkapn,转载请注明原文链接:https://www.cnblogs.com/cwkapn/p/18701619


浙公网安备 33010602011771号