FHQ Treap

2025-03-20T19:54:00+08:00 | 10分钟阅读 | 更新于 2025-03-20T19:54:00+08:00

@
FHQ Treap

玩原神玩的。

一些对 FHQ Treap 的简单介绍。

二叉搜索树

在动态进行区间操作时,常常需要数据结构来优化类似 $\mathcal O(n^2)$ 的时间复杂度,二叉搜索树 是一种优化复杂度的数据结构,定义如下:

  1. 二叉搜索树是一颗二叉树。
  2. 对于一个节点 $R$,若存在左子树,则 $R$ 的左子树所有点均小于 $R$ 的权值;若存在右子树,则 $R$ 的右子树的权值均大于 $R$ 的权值。

1

一棵二叉搜索树的示例

在进行动态查询操作时,显然操作的复杂度取决于树高。期望高度为 $\mathcal O(\log n)$,最劣情况为 $\mathcal O(n)$。故下介绍一种维护二叉搜索树的一种方式。

Treap

Treap,顾名思义,“树堆”。意即采用类似堆的性质来维护整棵二叉搜索树。 每个节点具有其权值 $val$,以及优先级 $rd$。其中权值满足二叉搜索树性质,优先级满足堆的性质。

人物

上图的优先级标注

然而问题便在于如何给节点的优先级赋值来达到期望的树高 $\mathcal O(\log n)$。 如若直接按照权值大小之类赋值,那么极易用一段有序的数据将树高卡为 $\mathcal O(n)$, 因此我们给所有点的优先级 随机赋值 来达到较为平衡的状态,那整个树的维护便得心应手了。

Treap 有两种实现方式,一种是 有旋Treap 一种是 无旋Treap(FHQ-treap)。

FHQ-Treap

FHQ-Treap 以其易懂、简洁、高效等优势而闻名,支持可持久化、维护序列等操作,由国家队成员范浩强提出。

基本操作

  1. 新建节点
1
2
3
4
5
int newnode (int v) { 
	rd[++ tot] = rnd ();  
	val[tot]   = v;
	return tot;
}
  1. 更新节点信息
1
2
3
void pushup (int x) {
	sz[x] = sz[ ls[x] ] + sz[ rs[x] ] + 1;
}

当然,如若这个节点还有其他信息,亦在此进行更新.


FHQ-Treap 的核心操作是 分裂(split)合并(merge),通过这两个操作可以实现许多功能。下给出两种操作的具体步骤。

分裂

  1. 按值分裂 不妨设当前以 $v$ 为值进行分裂,那么整棵树 $T$ 将会分为所有 $val_p\le v$ 的子树 $T_x$ 以及所有 $val_p>v$ 的子树 $T_y$。设当前树的根节点为 $R$,那么根据二叉搜索树的性质,若 $val[R]\le v$,则 $T_y$ 必在其右子树上,而 $T_x$ 可以暂时认为当前即为 $R$,反之亦然。这样递归的进行下去,便将一棵树分裂成两棵。 代码如下:
1
2
3
4
5
6
void split (int R, int v, int& x, int& y) {
	if (!R) return void (x = y = 0);
    if (val[R] <= v) split (rs[R], v, rs[x = R], y);
    else             split (ls[R], v, x, ls[y = R]);
	pushup (R);
}
  1. 按大小分裂 与上种方式类似,常用于进行 区间操作。设当前递归的树的根节点为 $R$,要分裂出 大小为 $len$ 的树为 $T_x$,剩下的树为 $T_y$。若 $sz[ls_R]\ge len$,那么 $T_x$ 必在 $R$ 的左子树上,同样的,$T_y$ 可以暂时的认为是 $R$,向 $ls_R$ 递归下去。 代码如下:
1
2
3
4
5
6
void split (int R, int len, int& x, int& y) {
    if (!R) return void (x = y = 0);
    if (sz[ ls[R] ] >= len) split (ls[R], len, x, ls[y = R]);
    else                    split (rs[R], len - sz[ ls[R] ] - 1, rs[x = R], y);
    pushup (R);
}

合并

对于两棵平衡树 $T_x$ 以及 $T_y$,合并的前提条件是:$\forall i\in T_x,\forall j\in T_y$ 都满足 $val_i\le val_j$. 考虑当前要合并的两点为 $i$,$j$。在 Treap 的定义中,我们知道树的结构要满足 的性质,引入了 优先级 $rd$,这种参数就是为合并操作服务的。 若 $rd_i>rd_j$,那么 $i$ 为 $j$ 的父节点,由于 $val_i<val_j$,故保持 $i$ 节点的左子树不变,修改其右子树。形式化的,$rs(i)\gets \operatorname{merge}(rs_i,j)$,其中 $\operatorname {merge}$ 函数返回的是最终合并树的根节点。反之亦然。 代码如下:

1
2
3
4
5
int merge (int i, int j) {
	if (!i || !j) return i + j; // 如若有一个节点为空,那么无需合并,返回非空节点即可
	if (rd[i] > rd[j]) return rs[i] = merge (rs[i], j), pushup (i), i;
	else			   return ls[j] = merge (i, ls[j]), pushup (j), j;
}

普通平衡树

实现以下操作即可:

  1. 插入操作
  • 往平衡树中插入一个大小为 $v$ 的节点。

只需要按照 $v$ 大小分裂成一棵 $val_x\le v$ 的平衡树 $T_x$ 以及一棵 $val_y>v$ 的平衡树 $T_y$,后合并 $T_x$ 与新节点,再将合并后的树与 $T_y$ 合并即可。注意合并顺序。

1
2
3
4
void insert (int v) {
    split (root, v, x, y);
    root = merge ( merge (x, newnode (v)), y );
}
  1. 删除操作
  • 删除平衡树中 一个 大小为 $v$ 的节点。

与上分裂相同,在分裂后的 $T_x$ 的再进行一次以 $v-1$ 为值分裂成 $T’_x$ 与 $T_z$,那么 $\forall z\in T_z,val_z=v$. 我们令 $z\gets\operatorname{merge}(ls_z,rs_z)$,根节点自然便被删除掉了。最后 按顺序 合并 $x,z,y$ 即可。

1
2
3
4
5
6
void del (int v) {
    split (root, v, x, y);
    split (x, v - 1, x, z);
    z = merge (ls[z], rs[z]);
    root = merge (merge (x, z), y);
}
  1. 排名查询
  • 查询 $\sum\limits_{x\in T}[val_x<v]+1$。

按照 $v-1$ 分裂成 $T_x$ 与 $T_y$,答案即为 $sz[T_x]+1$。最后记得合并回去。

1
2
3
4
5
6
int rank (int v) {
    split (root, v - 1, x, y);
    int ret = t[x].siz + 1;
    root = merge (x, y);
    return ret;
}
  1. 查询第 $k$ 小的数
  • 查询树中排名为 $k$ 的数。

从根节点不断递归,若 $sz[ls_R]+1=k$,那么 $val_R$ 即为答案;若 $sz[ls_R]+1<k$,那么答案在 $R$ 的右子树上,$k\gets k-(sz[ls_R]+1)$,并 $R\gets rs_R$;若 $sz[ls_R]+1> k$,那么答案在左子树上。

1
2
3
4
5
6
7
8
9
int Value (int k) {
    int R = root;
    while (rt) {
        if (sz[ ls[R] ] + 1 == k) break ;
        else if (sz[ ls[R] ] + 1 < k) k -= sz[ ls[R] ] + 1, R = rs[R];
        else R = ls[R];
    }
    return v[R];
}
  1. 前驱查询
  • 查询比 $v$ 小的最大数。

按照 $v$ 分裂为 $T_x$ 与 $T_y$,从 $x$ 不断向右子树递归,直到没有右子树停止,注意合并前保存答案,否则会被更改。

1
2
3
4
5
6
7
8
int prev (int v) {
    split (root, v - 1, x, y);
	int R = x;
	while (rs[R]) R = rs[R];
	int ans = val[R];
	root = merge (x, y);
	return ans;
}
  1. 后继查询
  • 查询比 $v$ 大的最小数。

按照 $v$ 分裂为 $T_x$ 与 $T_y$,从 $y$ 不断向左子树递归,直到没有左子树停止,注意合并前保存答案,否则会被更改。

1
2
3
4
5
6
7
8
int suff (int v) {
    split (root, v, x, y);
    int R = y;
    while (ls[R]) R = ls[R];
	int ans = val[R]; 
	root = merge (x, y);
    return ans;
}

了解完这些操作后,初学者理应理解了合并操作的 顺序 问题,同时也加深了对 分裂(split)合并(merge) 操作的理解。


文艺平衡树

给出序列 $a={1,2,\cdots,n}$,进行 $m$ 次区间翻转操作,求操作进行完的序列。

我们前面提到了 按大小分裂 的形式,这道题很好的应用了这种分裂方式来维护序列。

尚未操作时,树的中序遍历显然是 $1,2\cdots,n$。考虑翻转操作怎么写。假设当前要翻转的区间为 $[l,r]$,那么根据平衡树的性质,我们从根节点按照树大小 $r$ 分裂出子树 $T_x$ 与 $T_z$,再以 $x$ 为根,大小为 $l-1$ 分裂出子树 $T’_x$ 与 $T_y$。为了增强初学者的理解,我们列出三棵子树所维护的区间:$T_x:[1,l-1],T_y:[l,r],T_z:[r+1,n]$(不考虑空集)。那么我们只需对 $T_y$ 进行操作即可。 我们是由中序遍历来得到答案的,那么必须要遍历完 $y$ 的左子树才能到 $y$,之后才是 $y$ 的右子树。这总体是一个 左根右递归 过程。设代表的 $l$ 节点为 $i$,代表 $r$ 的节点为 $j$。遍历的过程中,我们只需要交换 $i$ 与 $j$ 便可交换序列中 $l$ 与 $r$ 的值。推广来说,我们只需要 递归 地交换 $ls_y$ 与 $rs_y$ 即可实现整段区间的翻转。可以从子树大小等多种角度理解这一过程,建议初学者画图来加深这一过程的理解。 但是,这样做的时间复杂度与直接暴力无异。我们类比线段树的 $tag$ 记号,在操作上打上 $tag$,并引入下传操作。时间复杂度便可期望地做到 $\mathcal O(n\log n)$。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <chrono>
#include <random>
#include <iostream>

using namespace std;

const int MAXN = 1e6 + 10;

mt19937 rnd (chrono :: steady_clock :: now ().time_since_epoch ().count ());

unsigned tot, rd[MAXN];
int root, val[MAXN], sz[MAXN], ls[MAXN], rs[MAXN], tag[MAXN], x, y, z;

void pushup (int x) { sz[x] = sz[ ls[x] ] + sz[ rs[x] ] + 1; }
void pushdown (int x) { if (tag[x]) tag[ ls[x] ] ^= 1, tag[ rs[x] ] ^= 1, swap (ls[x], rs[x]), tag[x] = 0; }
int newnode (int v) { rd[++ tot] = rnd (); val[tot] = v; sz[tot] = 1; return tot;}

void split (int R, int len, int& x, int& y) {
    if (!R) return void (x = y = 0);
    pushdown (R);
    if (sz[ ls[R] ] >= len) split (ls[R], len, x, ls[y = R]);
    else                    split (rs[R], len - sz[ ls[R] ] - 1, rs[x = R], y);
    pushup (R);
}

int merge (int x, int y) {
    if (!x || !y) return x | y;
    pushdown (x); pushdown (y);
    if (rd[x] > rd[y]) return rs[x] = merge (rs[x], y), pushup (x), x;
    else               return ls[y] = merge (x, ls[y]), pushup (y), y;
}

void modify (int l, int r) { split (root, r, x, z); split (x, l - 1, x, y); tag[y] ^= 1; root = merge (x, merge (y, z)); }
void output (int x) { if (!x) return; pushdown (x); output (ls[x]); printf ("%d ", val[x]); output (rs[x]); }

int main (void) {

    int n, m;
    scanf ("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) root = merge (root, newnode (i));
    while (m --) { int l, r; scanf ("%d%d", &l, &r); modify (l, r); }
    output (root);

    return 0;
}

P4146 序列终结者

维护长度为 $N$ 的序列 $a_n$,每次操作可以:给 $[l,r]$ 中每个数加上 $v$;翻转区间 $[l,r]$;查询 $\max\limits_{i\in[l,r]}a_i$。

给每个节点多加一个参数 $mx$ 表示最大值,类比线段树操作再加一个 $add$ 标记,代码与上题无异。 记得开 long long

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <chrono>
#include <random>
#include <iostream>
#include <algorithm>

#define int long long

using namespace std;

const int MAXN = 2e5;

mt19937 rnd (chrono :: steady_clock :: now ().time_since_epoch ().count ());

int rd[MAXN];
int tot, root, val[MAXN], mx[MAXN], sz[MAXN], tag[MAXN], add[MAXN], ls[MAXN], rs[MAXN], x, y, z;

int newnode () { rd[++ tot] = rnd (); sz[tot] = 1;  return tot; }
void pushdown (int x) { if (tag[x]) tag[ ls[x] ] ^= 1, tag[ rs[x] ] ^= 1, swap (ls[x], rs[x]), tag[x] = 0; if (add[x]) val[ ls[x] ] += add[x], val[ rs[x] ] += add[x], mx[ ls[x] ] += add[x], mx[ rs[x] ] += add[x], add[ ls[x] ] += add[x], add[ rs[x] ] += add[x], add[x] = 0;}
void pushup (int x) { sz[x] = sz[ ls[x] ] + sz[ rs[x] ] + 1; mx[x] = max ({mx[ ls[x] ], mx[ rs[x] ], val[x]}); }

void split (int R, int len, int& x, int& y) {
    if (!R) return void (x = y = 0);
    pushdown (R);
    if (sz[ ls[R] ] >= len) split (ls[R], len, x, ls[y = R]);
    else                    split (rs[R], len - sz[ ls[R] ] - 1, rs[x = R], y);
    pushup (R);
}

int merge (int x, int y) {
    if (!x || !y) return x | y;
    pushdown (x); pushdown (y);
    if (rd[x] > rd[y]) return rs[x] = merge (rs[x], y), pushup (x), x;
    else               return ls[y] = merge (x, ls[y]), pushup (y), y;
}

signed main (void) {

    int n, q;
    scanf ("%lld%lld", &n, &q); val[0] = add[0] = mx[0] = -1e16;
    for (int i = 1; i <= n; i ++) root = merge (root, newnode ());
    while (q --) {
        int k, l, r, v;
        scanf ("%lld%lld%lld", &k, &l, &r);
        if (k == 1)  {
            scanf ("%lld", &v);
            split (root, r, x, z); split (x, l - 1, x, y);
            add[y] += v, mx[y] += v, val[y] += v;
            root = merge (merge (x, y), z);
        } else if (k == 2) {
            split (root, r, x, z); split (x, l - 1, x, y);
            tag[y] ^= 1;
            root = merge (merge (x, y), z);
        } else {
            split (root, r, x, z); split (x, l - 1, x, y);
            printf ("%lld\n", mx[y]); root = merge (merge (x, y), z);
        }
    }

    return 0;
}

P3215 [HNOI2011] 括号修复 / [JSOI2011] 括号序列

动态查询修改一段串使其变为合法括号串的最小修改次数。

用到一个 Trick: 对于一个圆括号串 $s$ 我们设 $f(x)=\begin{cases}-1,& x=\texttt(\1,&x=\texttt)\end{cases}$, 对于区间 $s[l,r]$, $pre$ 表示关于 $f(x)$ 的前缀最大值,$suf$ 表示关于 $f(x)$ 的后缀最小值,那么使 $s$ 成为合法括号串的最小修改次数为 $\lceil\dfrac{pre}2\rceil+\lceil\dfrac{-suf}2\rceil$.

Proof 我们把所有已经匹配的括号删除,则剩下的串形如 ))))...((.. 设右括号的数量为 $x$,左括号的数量为 $y$ 由于删除的串的贡献对于 $pre$ 与 $suf$ 来说必然是 $0$,那么 $pre=x$,$suf=-y$ 我们尝试设计一种方案来使其操作数最小 不妨考虑右括号。对于长度为 $x$ 的连续右括号来说,我们只需要将左侧 $\lfloor\dfrac{x}2\rfloor$ 变为右括号,就可以完全匹配。如果为奇数个,那么由于有解性,右侧的左括号必然为奇数个。这样只需要再多变换一个,即可完全匹配。因此对于右括号而言,答案为 $\lceil\dfrac{x}2\rceil$ 综上,答案为 $\lceil\dfrac{x}2\rceil+\lceil\dfrac{y}2\rceil=\lceil\dfrac{pre}2\rceil+\lceil\dfrac{-suf}2\rceil$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <chrono>
#include <random>
#include <iostream>

using namespace std;

const int MAXN = 2.5e5 + 10;

mt19937 rnd (chrono :: steady_clock :: now ().time_since_epoch ().count ());

int n, q;
char s[MAXN];
unsigned tot, rd[MAXN];
int root, typ[MAXN], sum[MAXN], ls[MAXN], rs[MAXN], sz[MAXN], t_sw[MAXN], t_md[MAXN], t_iv[MAXN], premx[MAXN], premn[MAXN], sufmx[MAXN], sufmn[MAXN], x, y, z;

void replace (int v, int R) {
    if (!R || !v) return ;
    sum[R] = v * sz[R];
    t_md[R] = typ[R] = v;
    if (v == 1) premn[R] = sufmn[R] = 0, premx[R] = sufmx[R] = sum[R];
    else        premn[R] = sufmn[R] = sum[R], premx[R] = sufmx[R] = 0; 
}

void swap (int R) {
    if (!R) return ;
    swap (ls[R], rs[R]);
    swap (premn[R], sufmn[R]); swap (premx[R], sufmx[R]);
    t_sw[R] ^= 1;
}

void invert (int R) {
    if (!R) return ;
    typ[R] *= -1; sum[R] *= -1; t_md[R] *= -1;
    t_iv[R] ^= 1;    
    swap (premn[R], premx[R]); swap (sufmx[R], sufmn[R]);
    premn[R] *= -1, premx[R] *= -1, sufmx[R] *= -1, sufmn[R] *= -1;
}

void pushup (int x) { if (!x) return ; sum[x] = sum[ ls[x] ] + sum[ rs[x] ] + typ[x]; sz[x] = sz[ ls[x] ] + sz[ rs[x] ] + 1; premx[x] = max (premx[ ls[x] ], sum[ ls[x] ] + typ[x] + premx[ rs[x] ]); premn[x] = min (premn[ ls[x] ], sum[ ls[x] ] + typ[x] + premn[ rs[x] ]); sufmx[x] = max (sufmx[ rs[x] ], sum[  rs[x] ] + typ[x] + sufmx[ ls[x] ]); sufmn[x] = min (sufmn[ rs[x] ], sum[ rs[x] ] + typ[x] + sufmn[ ls[x] ]);}
void pushdown (int x) { if (!x) return ; if (t_iv[x]) invert (ls[x]), invert (rs[x]), t_iv[x] = 0; if (t_md[x]) replace (t_md[x], ls[x]), replace (t_md[x], rs[x]), t_md[x] = 0; if (t_sw[x]) swap (ls[x]), swap (rs[x]), t_sw[x] = 0; }
int newnode (int tp) { ++ tot; rd[tot] = rnd (); premx[tot] = sufmx[tot] = (tp == 1); premn[tot] = sufmn[tot] = -1 * (tp == -1); sz[tot] = 1; sum[tot] = typ[tot] = tp; return tot; }

void split (int R, int len, int& x, int& y) {
    if (!R) return void (x = y = 0);
    pushdown (R);
    if (sz[ ls[R] ] >= len) split (ls[R], len, x, ls[y = R]);
    else                    split (rs[R], len - sz[ ls[R] ] - 1, rs[x = R], y);
    pushup (R);
}

int merge (int x, int y) {
    if (!x || !y) return x | y;
    pushdown (x), pushdown (y);
    if (rd[x] > rd[y]) return rs[x] = merge (rs[x], y), pushup (x), x;
    else               return ls[y] = merge (x, ls[y]), pushup (y), y;
}

int main (void) {

    scanf ("%d%d", &n, &q);
    scanf ("%s", s + 1);
    for (int i = 1; i <= n; i ++) root = merge (root, newnode (s[i] == ')' ? 1 : -1));
    while (q --) {
        char opt[20];
        int l, r;
        scanf ("%s%d%d", opt + 1, &l, &r);
        if (opt[1] == 'R') {
            scanf ("%s", opt + 1);
            split (root, r, x, z); split (x, l - 1, x, y);
            replace ((opt[1] == ')' ? 1 : -1), y);
            root = merge (merge (x, y), z);
        }
        else if (opt[1] == 'S') {
            split (root, r, x, z); split (x, l - 1, x, y);
            swap (y);
            root = merge (merge (x, y), z);
        }
        else if (opt[1] == 'I') {
            split (root, r, x, z); split (x, l - 1, x, y);
            invert (y);
            root = merge (merge (x, y), z);
        }
        else {  
            split (root, r, x, z); split (x, l - 1, x, y);
            printf ("%d\n", (1 + premx[y]) / 2 + (-sufmn[y] + 1) / 2);
            root = merge (merge (x, y), z);
        }
    }

    return 0;
}

© 2025 Toorean's Blog

🌱 Powered by Hugo with theme Dream.