平衡树 FHQ-Treap

二叉搜索树

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

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

一棵二叉搜索树的示例

在进行动态查询操作时,显然操作的复杂度取决于树高。期望高度为 \(\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. 新建节点
int newnode (int v) { 
	rd[++ tot] = rnd ();  
	val[tot]   = v;
	return tot;
}
  1. 更新节点信息
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\),反之亦然。这样递归的进行下去,便将一棵树分裂成两棵。
    代码如下:
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\) 递归下去。
    代码如下:
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}\) 函数返回的是最终合并树的根节点。反之亦然。
代码如下:

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\) 合并即可。注意合并顺序。

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\) 即可。

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\)。最后记得合并回去。

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\),那么答案在左子树上。

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\) 不断向右子树递归,直到没有右子树停止,注意合并前保存答案,否则会被更改。

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\) 不断向左子树递归,直到没有左子树停止,注意合并前保存答案,否则会被更改。

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)\)

#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

#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\)

#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;
}
posted @ 2025-03-20 19:54  Toorean  阅读(51)  评论(0)    收藏  举报