「学习笔记」平衡树基础:Splay 和 Treap

点击查看目录

知识点

平衡树概述

二叉搜索树(BST)的简单定义:

  • 根节点的左子树权值 \(<\) 根节点权值 \(<\) 根节点的右子树权值;
  • 左子树和右子树均为二叉搜索树。

这样的数据结构可以维护一个集合的以下操作:

  • 查找最小/最大值;
  • 插入一个元素;
  • 删除一个元素;
  • 求元素的排名;
  • 查找排名为 \(k\) 的元素。

该数据结构最优情况下单次查询仅需 \(\Theta(\log_2{n})\) 的时间复杂度,但通过构造输入可以使二叉搜索树退化为链,达到 \(\Theta(n)\) 的时间复杂度。

所以我们要让这棵二叉搜索树尽量平衡(深度接近 \(\log_2{n}\))。

于是就诞生了平衡树。

Splay

旋转操作

可以发现左旋/右旋后树的形态发生变化,但仍然满足二叉搜索树的性质。

Splay 操作

Splay 的核心操作,即把一个点提到根

分三种情况:

  1. 爹就是根:直接转(\(\text{zig}\))

  2. 三点共线:先转爹,再转自己(\(\text{zig-zig}\))

  3. 三点不共线:直接转两下自己(\(\text{zig-zag}\)):

为什么要这么转呢?因为直接单旋上去无法保证复杂度,随随便便就能卡掉,而双旋的时间复杂度可以用势能分析法进行分析

插入 \(x\)

根据 BST 的性质找到一个地方插入新点,然后把它旋上去。

查询排名为 \(k\) 的数

假设当前到了点 \(p\):

  • \(k\) 小于 \(p\) 的左子树大小时往左走
  • 否则令 \(k\) 减去 \(p\) 的左子树大小,然后:
    • 如果当前节点副本数大于等于 \(k\),则当前节点的权值就是答案,然后把这个点旋上去。
    • 否则令 \(k\) 减去 \(p\) 的副本数。

查询 \(x\) 的排名

找到 \(x\) 提到根,左子树的大小加 \(1\) 就是答案。

查询 \(x\) 的前驱

把 \(x\) 提到根后,在左子树里一路往右走。

查询 \(x\) 的后继

把 \(x\) 提到根后,在右子树里一路往左走。

删除 \(x\)

最麻烦的一个。

首先我们把 \(x\) 提到根

  • 此时如果 \(x\) 副本数大于 \(1\) 则直接让副本数减一。
  • 否则看儿子数量:
    • 没儿子了:直接删。
    • 一个儿子:让儿子当根。
    • 两个儿子:首先把 \(x\) 的前驱提上来,因为刚才 \(x\) 是根并且最后一次旋转的时候不可能三点共线(不然就不是左子树的最大值了),所以此时根的右儿子是 \(x\),且此时 \(x\) 无左儿子,所以直接把 \(x\) 的右儿子接到根的右儿子上就可以了。

代码

点击查看代码
const ll N = 2e5 + 10, INF = 1ll << 40;
namespace Splay {
class TreePoint {
public:
ll val, cnt, sz;
ll fa, son[2];
inline void New (ll num) { val = num, cnt = sz = 1, fa = son[0] = son[1] = 0; return; }
inline void Clear () { val = cnt = sz = fa = son[0] = son[1] = 0; return; }
}; class splay {
public:
TreePoint tr[N];
ll tot = 0, rt = 0; #define va(p) tr[p].val
#define cn(p) tr[p].cnt
#define sz(p) tr[p].sz
#define fa(p) tr[p].fa
#define so(p, lr) tr[p].son[lr] inline void PushUp (ll p) { sz (p) = sz (so (p, 0)) + sz (so (p, 1)) + cn (p); return; } // Update the size.
inline bool Get (ll p) { return p == so (fa (p), 1); } // Left son(0) or right son(1)?
inline ll NewPoint (ll num) { tr[++tot].New (num); return tot; } // Build a new Point. inline void Rotate (ll p) {
ll q = fa (p), r = fa (q), sd = Get (p);
so (q, sd) = so (p, sd ^ 1);
if (so (p, sd ^ 1)) fa (so (p, sd ^ 1)) = q;
so (p, sd ^ 1) = q, fa (q) = p, fa (p) = r;
if (r) so (r, q == so (r, 1)) = p;
PushUp (q), PushUp (p);
return;
} // Zig or zag.
inline void Splay (ll p) {
for (ll q = fa (p); q = fa (p), q; Rotate (p))
if (fa (q)) Rotate (Get (p) == Get (q) ? q : p);
rt = p;
return;
} // Splay's core operations! inline void Out () {
_for (i, 1, tot) {
printf ("%lld : val=%lld cnt=%lld sz=%lld fa=%lld %lld %lld\n", i, va (i), cn (i), sz (i), fa (i), so (i, 0), so (i, 1));
}
puts ("");
return;
} // For debug. inline void Insert (ll x) {
if (!rt) rt = NewPoint (x), PushUp (rt);
else {
ll p = rt;
while (1) {
ll sd = (bool)(va (p) < x);
if (va (p) == x) {
++cn (p);
PushUp (p), PushUp (fa (p));
break;
}
else if (so (p, sd)) p = so (p, sd);
else {
so (p, sd) = NewPoint (x);
fa (so (p, sd)) = p, p = so (p, sd);
PushUp (p), PushUp (fa (p));
break;
}
}
Splay (p);
}
return;
} // Insert a number x.
inline ll GetRank (ll x) {
ll p = rt, cnt = 1;
while (p) {
if (va (p) <= x) {
cnt += sz (so (p, 0));
if (va (p) == x) break;
cnt += cn (p);
p = so (p, 1);
}
else p = so (p, 0);
}
Splay (p);
return cnt;
} // Get x's rank.
inline ll GetKth (ll x) {
ll p = rt;
while (p) {
if (sz (so (p, 0)) < x) {
x -= sz (so (p, 0));
if (cn (p) >= x) break;
x -= cn (p);
p = so (p, 1);
}
else p = so (p, 0);
}
Splay (p);
return va (p);
} // Get k-th number.
inline ll RealPre () {
ll p = so (rt, 0);
if (p) {
while (so (p, 1)) p = so (p, 1);
Splay (p);
}
return p;
}
inline void Delete (ll x) {
GetRank (x);
ll sd = (bool)(so (rt, 1));
if (cn (rt) > 1) --cn (rt), PushUp (rt);
else if (!so (rt, 0) && !so (rt, 1)) tr[rt].Clear (), rt = 0;
else if (so (rt, 0) && so (rt, 1)) {
ll p = rt, q = RealPre ();
fa (so (p, 1)) = q;
so (q, 1) = so (p, 1);
tr[p].Clear ();
PushUp (rt);
}
else {
ll q = so (rt, sd);
tr[rt].Clear ();
fa (rt = q) = 0;
}
return;
} // Delete a number x.
inline ll Pre (ll x) {
Insert (x);
ll ans = va (RealPre ());
Delete (x);
return ans;
} // Get x's pre.
inline ll Next (ll x) {
Insert (x);
ll p = so (rt, 1), ans = 0;
if (p) {
while (so (p, 0)) p = so (p, 0);
Splay (p);
}
ans = va (rt);
Delete (x);
return ans;
} // Get x's next. #undef va
#undef cn
#undef sz
#undef fa
#undef so
};
}

替罪羊树

很暴力的一个东西。

定义一个平衡因子 \(\alpha\)(最好选 \(0.7\sim0.8\)),插入/删除一个节点的时候检查是否存在一个节点的子树的大小乘上 \(\alpha\) 小于左/右儿子树的大小,如果有则把这棵子树直接拍平重构。

其他操作和普通 BST 一样。

点击查看代码
namespace ScapeGoatTree {
const ldb alpha = 0.75;
const ll N = 1e5 + 10;
class TreePoint {
public:
ll val, cnt, sz, cp, del;
ll fa, son[2];
};
class SGT {
private:
ll tot = 0, rt = 0, dp[N], cd = 0;
TreePoint tr[N];
vec tmp, c; #define va(p) tr[p].val
#define cn(p) tr[p].cnt
#define sz(p) tr[p].sz
#define de(p) tr[p].del
#define cp(p) tr[p].cp
#define fa(p) tr[p].fa
#define so(p, lr) tr[p].son[lr] inline ll NewP (ll num) {
ll p = cd ? dp[cd--] : ++tot;
va (p) = num;
cn (p) = sz (p) = 1;
cp (p) = fa (p) = so (p, 0) = so (p, 1) = de (p) = 0;
return p;
}
inline void DelP (ll p) {
va (p) = cn (p) = sz (p) = cp (p) = fa (p) = so (p, 0) = so (p, 1) = de (p) = 0;
dp[++cd] = p;
return;
}
inline void PushUp (ll p) {
sz (p) = (de (p) ? 0 : cn (p)) + sz (so (p, 0)) + sz (so (p, 1));
cp (p) = 1 + cp (so (p, 0)) + cp (so (p, 1));
return;
} public:
inline void Clap (ll p) {
if (so (p, 0)) Clap (so (p, 0));
if (!de (p)) tmp.push_back (va (p)), c.push_back (cn (p));
if (so (p, 1)) Clap (so (p, 1));
DelP (p);
return;
}
inline ll PullUp (ll l, ll r, ll fat) {
if (l > r) return 0;
bdmd;
ll p = NewP (tmp[mid]);
cn (p) = c[mid], fa (p) = fat;
if (l < mid) so (p, 0) = PullUp (l, mid - 1, p);
if (mid < r) so (p, 1) = PullUp (mid + 1, r, p);
PushUp (p);
return p;
}
inline void ReBuild (ll p) {
ll q = fa (p);
tmp.clear (), c.clear ();
Clap (p);
if (!q) rt = PullUp (0, tmp.size () - 1, q);
else so (q, p == so (q, 1)) = PullUp (0, tmp.size () - 1, q);
return;
} inline bool Bad (ll p) {
return cp (so (p, 0)) > cp (p) * alpha || cp (so (p, 1)) > cp (p) * alpha;
}
inline void Check (ll p) {
ll q = 0;
while (p) {
if (Bad (p)) q = p;
rt = p;
PushUp (p);
p = fa (p);
}
if (q) ReBuild (q);
return;
} inline void Insert (ll num) {
ll p = rt;
if (!rt) {
rt = NewP (num);
return;
}
while (1) {
if (va (p) == num) {
++cn (p);
if (de (p)) de (p) = 0;
Check (p);
return;
}
ll sd = num > va (p);
if (so (p, sd)) p = so (p, sd);
else {
so (p, sd) = NewP (num);
fa (so (p, sd)) = p;
Check (p);
return;
}
}
return;
}
inline void Delete (ll num) {
ll p = rt;
while (p) {
if (va (p) == num) {
--cn (p);
if (cn (p) < 1) {
cn (p) = 0;
de (p) = 1;
}
Check (p);
return;
}
ll sd = num > va (p);
p = so (p, sd);
}
return;
}
inline ll GetRank (ll num) {
ll p = rt, rk = 1;
while (p) {
if (va (p) > num) {
p = so (p, 0);
continue;
}
rk += sz (so (p, 0));
if (num == va (p)) break;
rk += cn (p);
p = so (p, 1);
}
return rk;
}
inline ll GetKth (ll k) {
ll p = rt;
while (p) {
if (sz (so (p, 0)) >= k) {
p = so (p, 0);
continue;
}
k -= sz (so (p, 0));
if (k <= cn (p)) break;
k -= cn (p);
p = so (p, 1);
}
return va (p);
}
inline ll Pre (ll num) { return GetKth (GetRank (num) - 1); }
inline ll Nxt (ll num) { return GetKth (GetRank (num + 1)); } #undef va
#undef cn
#undef sz
#undef de
#undef cp
#undef fa
#undef so
};
}

Treap

每个节点还要存一个随机权值,使得整棵树不仅对于原权值来说是一棵 BST,对于随机权值来说还是一个堆,在随机状态下一棵 Treap 是比较 \(\log_2n\) 层的。当然不排除你脸太黑导致 Treap 退化成链的极小可能。

那么插入的时候,如果新节点不满足堆的性质了,需要往上旋转。删除的时候直接旋到叶子结点删掉,或者只剩一个儿子的时候直接让儿子代替自己。

其他操作和普通 BST 一样。

点击查看代码
namespace TREAP {
class TreePoint {
public:
ll val, rk, sz, cnt;
ll son[2];
inline void NewP (ll x) { val = x, rk = rand (), cnt = sz = 1, son[0] = son[1] = 0;return; }
};
class Treap {
public:
TreePoint tr[N];
ll tot = 0, rt = 0; #define va(p) tr[p].val
#define rk(p) tr[p].rk
#define sz(p) tr[p].sz
#define cn(p) tr[p].cnt
#define so(p, lr) tr[p].son[lr] inline void PushUp (ll p) { sz (p) = cn (p) + sz (so (p, 0)) + sz (so (p, 1)); return; }
inline ll NewP (ll num) { tr[++tot].NewP (num); return tot; }
inline void DelP (ll p) { va (p) = rk (p) = sz (p) = cn (p) = so (p, 0) = so (p, 1) = 0;return; } inline void Rotate (ll& p, ll sd) {
ll q = so (p, sd ^ 1);
so (p, sd ^ 1) = so (q, sd);
so (q, sd) = p, p = q;
PushUp (so (q, sd)), PushUp (q);
return;
} void Insert (ll& p, ll x) {
if (!p) {
p = NewP (x);
return;
}
if (va (p) == x) ++cn (p);
else {
bool sd = (va (p) < x);
Insert (so (p, sd), x);
if (rk (so (p, sd)) > rk (p)) Rotate (p, sd ^ 1);
}
PushUp (p);
return;
}
void Delete (ll& p, ll x) {
if (!p) return;
if (va (p) == x) {
if (cn (p) == 1) {
if (!so (p, 0) && !so (p, 1)) DelP (p), p = 0;
else if (!so (p, 0) || !so (p, 1)) p = so (p, 0) + so (p, 1);
else {
ll sd = (rk (so (p, 1)) < rk (so (p, 0)));
Rotate (p, sd), Delete (so (p, sd), x);
}
}
else --cn (p);
}
else Delete (so (p, (x > va (p))), x);
PushUp (p);
return;
}
ll GetRank (ll p, ll x) {
if (!p) return 1;
if (va (p) == x) return 1 + sz (so (p, 0));
if (va (p) < x) return GetRank (so (p, 1), x) + sz (so (p, 0)) + cn (p);
return GetRank (so (p, 0), x);
}
ll GetKth (ll p, ll x) {
if (!p) return 0;
if (x <= sz (so (p, 0))) return GetKth (so (p, 0), x);
x -= sz (so (p, 0));
if (x <= cn (p)) return va (p);
x -= cn (p);
return GetKth (so (p, 1), x);
}
ll Pre (ll x) { return GetKth (rt, GetRank (rt, x) - 1); }
ll Next (ll x) { return GetKth (rt, GetRank (rt, x + 1)); } #undef va
#undef rk
#undef sz
#undef cn
#undef so
};
}

FHQ_Treap

FHQ_Treap 依旧满足 Treap 的性质,但是操作方式很神奇!

FHQ_Treap 也被称为无旋 Treap,因为它的所有操作都没有恶心的旋转,只有分裂和合并两个基础操作!

分裂有两种方法:按权值裂和按排名裂。一般来说,只当平衡树的时候通常按权值裂,维护序列的时候按排名裂。具体怎么裂见代码和例题。

合并的时候要保证第一棵树的所有权值都比第二棵树小,注意合的过程中要保证 Treap 的性质。

为了方便写我没有写副本数。

然后就是六个操作了:

  • 插入

    按 \(x\) 裂成 \(a,b\) 两棵树,然后按 \(a,x,b\) 的顺序合起来

  • 删除

    这绝对是删除操作最简单的平衡树了!先按 \(x\) 裂成 \(a,b\) 两棵树,再把 \(a\) 按 \(x-1\) 裂成 \(a,c\) 两棵树,此时

  • 查询排名为 \(k\) 的数

    和普通 BST 一样。

  • 查询 \(x\) 的排名

    按 \(x-1\) 裂成 \(a,b\) 两棵树,\(a\) 的大小加一就是答案

  • 查询 \(x\) 的前驱

    按 \(x-1\) 裂成 \(a,b\) 两棵树,\(a\) 里的最大值

  • 查询 \(x\) 的后继

    按 \(x-1\) 裂成 \(a,b\) 两棵树,\(b\) 里的最小值

点击查看代码
namespace FHQ_TREAP {
class TreeNode {
public:
ll val, rk, sz, son[2];
inline void Add (ll num) { val = num, rk = rand (), sz = 1, son[0] = son[1] = 0; return; }
};
class FHQTreap {
private:
TreeNode tr[N];
ll tot = 0, rt = 0, a, b, c; #define va(p) tr[p].val
#define rk(p) tr[p].rk
#define sz(p) tr[p].sz
#define so(p, lr) tr[p].son[lr] inline ll NewP (ll num) { tr[++tot].Add (num); return tot; }
inline void PushUp (ll p) { sz (p) = 1 + sz (so (p, 0)) + sz (so (p, 1)); return; } void Split (ll p, ll x, ll& l, ll& r) {
if (!p) l = r = 0;
else {
if (va (p) <= x) l = p, Split (so (p, 1), x, so (p, 1), r);
else r = p, Split (so (p, 0), x, l, so (p, 0));
PushUp (p);
}
return;
}
inline ll Merge (ll l, ll r) {
if (!l || !r) return l + r;
if (rk (l) < rk (r)) {
so (l, 1) = Merge (so (l, 1), r);
PushUp (l);
return l;
}
else {
so (r, 0) = Merge (l, so (r, 0));
PushUp (r);
return r;
}
} public:
inline void Insert (ll x) {
Split (rt, x, a, b);
rt = Merge (Merge (a, NewP (x)), b);
return;
}
inline void Delete (ll x) {
Split (rt, x, a, b);
Split (a, x - 1, a, c);
rt = Merge (Merge (a, Merge (so (c, 0), so (c, 1))), b);
return;
}
inline ll GetRank (ll x) {
Split (rt, x - 1, a, b);
ll ans = sz (a) + 1;
rt = Merge (a, b);
return ans;
}
inline ll GetKth (ll x) {
ll p = rt;
while (p) {
if (x <= sz (so (p, 0))) p = so (p, 0);
else {
x -= sz (so (p, 0)) + 1;
if (!x) break;
p = so (p, 1);
}
}
return va (p);
}
inline ll Pre (ll x) {
Split (rt, x - 1, a, b);
ll p = a;
while (so (p, 1)) p = so (p, 1);
rt = Merge (a, b);
return va (p);
}
inline ll Next (ll x) {
Split (rt, x, a, b);
ll p = b;
while (so (p, 0)) p = so (p, 0);
rt = Merge (a, b);
return va (p);
} #undef va
#undef rk
#undef sz
#undef so
};
}

树套树

用于维护单点修改和区间第 \(k\) 大,排名,前驱和后继的查询。

首先建立一棵线段树,每个节点单建一棵平衡树维护这个区间(线段树的每一层有 \(n\) 个节点,一共 \(\log_2n\) 层,因此只有 \(n\log_2n\) 个节点,不会 TLE/MLE)。

然后是如何维护五个操作:

  • 单点修改:所有包含这个点的平衡树删除原来这个点的数,再插入新数。
  • \(x\) 的区间排名:将在这个区间内的平衡树的 \(x\) 加起来。
  • \(x\) 的区间第 \(k\) 大:使用二分,找到一个数的区间排名是 \(k\)。
  • \(x\) 的区间前驱:这个区间内的所有平衡树中 \(x\) 的前驱最大值。
  • \(x\) 的区间后继:这个区间内的所有平衡树中 \(x\) 的后继最小值。
点击查看代码
const ll N = 1e5 + 10, inf = 2147483647;

namespace FHQ_TREAP {
class TreeNode {
public:
ll val, rk, sz, son[2];
inline void Add (ll num) noexcept { val = num, rk = rand (), sz = 1, son[0] = son[1] = 0; return; }
} tr[N << 4];
ll tot = 0, len = 0, free[N << 4]; #define va(p) tr[p].val
#define rk(p) tr[p].rk
#define sz(p) tr[p].sz
#define so(p, lr) tr[p].son[lr] class FHQTreap {
private:
ll rt = 0, a, b, c; inline void PushUp (ll p) noexcept { sz (p) = 1 + sz (so (p, 0)) + sz (so (p, 1)); }
inline ll NewP (ll num) noexcept {
ll p = len ? free[len--] : ++tot;
tr[p].Add (num);
return p;
}
inline void DelP (ll p) noexcept {
va (p) = rk (p) = sz (p) = so (p, 0) = so (p, 1) = 0;
free[++len] = p;
return;
} inline void Split (ll p, ll x, ll& l, ll& r) noexcept {
if (!p) l = r = 0;
else {
if (va (p) <= x) l = p, Split (so (p, 1), x, so (p, 1), r);
else r = p, Split (so (p, 0), x, l, so (p, 0));
PushUp (p);
}
return;
}
inline ll Merge (ll l, ll r) noexcept {
if (!l || !r) return l + r;
if (rk (l) > rk (r)) {
so (l, 1) = Merge (so (l, 1), r);
PushUp (l);
return l;
}
else {
so (r, 0) = Merge (l, so (r, 0));
PushUp (r);
return r;
}
} public:
inline void Insert (ll x) noexcept {
Split (rt, x, a, b);
rt = Merge (Merge (a, NewP (x)), b);
return;
}
inline void Delete (ll x) noexcept {
Split (rt, x, a, b);
Split (a, x - 1, a, c);
rt = Merge (Merge (a, Merge (so (c, 0), so (c, 1))), b);
DelP (c);
return;
}
inline ll GetRank (ll x) noexcept {
Split (rt, x - 1, a, b);
ll ans = sz (a);
rt = Merge (a, b);
return ans;
}
inline ll Pre (ll x) noexcept {
Split (rt, x - 1, a, b);
ll p = a;
while (so (p, 1)) p = so (p, 1);
rt = Merge (a, b);
return va (p);
}
inline ll Next (ll x) noexcept {
Split (rt, x, a, b);
ll p = b;
while (so (p, 0)) p = so (p, 0);
rt = Merge (a, b);
return va (p);
}
}; #undef va
#undef rk
#undef sz
#undef so
} namespace SEGMENT_TREE {
class SegmentTree {
private:
FHQ_TREAP::FHQTreap tr[N << 2]; #define ls(p) p << 1, l, mid
#define rs(p) p << 1 | 1, mid + 1, r public:
inline void Build (ll p, ll l, ll r, ll* a) noexcept {
if (l != r) {
ll mid = md;
Build (ls (p), a);
Build (rs (p), a);
}
tr[p].Insert (inf), tr[p].Insert (-inf);
_for (i, l, r) tr[p].Insert (a[i]);
return;
}
inline void Update (ll p, ll l, ll r, ll x, ll y, ll z) noexcept {
if (r < x || x < l) return;
if (l != r) {
ll mid = md;
Update (ls (p), x, y, z);
Update (rs (p), x, y, z);
}
tr[p].Delete (z), tr[p].Insert (y);
}
inline ll QueryRank (ll p, ll l, ll r, ll le, ll ri, ll x) noexcept {
if (ri < l || r < le) return 0;
ll mid = md;
if (le <= l && r <= ri) return tr[p].GetRank (x) - 1;
else return QueryRank (ls (p), le, ri, x) + QueryRank (rs (p), le, ri, x);
}
inline ll QueryKth (ll le, ll ri, ll x, ll n, ll mx) noexcept {
ll l = 0, r = mx, ans = 0;
while (l <= r) {
ll mid = md;
if (QueryRank (1, 1, n, le, ri, mid) + 1 <= x) ans = mid, l = mid + 1;
else r = mid - 1;
}
return ans;
}
inline ll QueryPre (ll p, ll l, ll r, ll le, ll ri, ll x) noexcept {
if (ri < l || r < le) return -inf;
ll mid = md;
if (le <= l && r <= ri) return tr[p].Pre (x);
else return std::max (QueryPre (ls (p), le, ri, x), QueryPre (rs (p), le, ri, x));
}
inline ll QueryNext (ll p, ll l, ll r, ll le, ll ri, ll x) noexcept {
if (ri < l || r < le) return inf;
ll mid = md;
if (le <= l && r <= ri) return tr[p].Next (x);
else return std::min (QueryNext (ls (p), le, ri, x), QueryNext (rs (p), le, ri, x));
}
};
}

平衡树的区间操作

平衡树不是只能平衡树,也可以进行区间操作。

用平衡树中序遍历顺序不变的性质,维护序列中每个数的前后位置(即把按数值排序变为按下标排序)。

操作时,把需要操作/查询的区间放在一颗子树上再直接对子树进行操作/查询。

如果操作对子树的所有点生效,应该给子树打个标记,旋转/分裂/合并的时候随手下传标记。

具体的

使用 Splay:通过对 \(l-1\) 和 \(r+1\) 提根使得区间 \([l, r]\) 在一颗子树上,然后对这棵子树进行操作。

使用 FHQ-Treap:把区间 \([l, r]\) 裂出来,然后对这棵子树进行操作,再合并回去。

平衡树还支持插入/删除一个点,所以维护序列的时候还可以在任意位置加入/删除一个数。

另外,FHQ-Treap 裂开合并的神奇操作还支持对一个区间进行移动。

例题

P3391 文艺平衡树

思路

对需要操作的区间打个翻转标记,同时交换其左右儿子。

P4036 [JSOI2008]火星人

思路

维护一棵子树的字符串的哈希值,然后就是裸的区间操作了。

P4309 [TJOI2013]最长上升子序列

思路

不难发现每次插入的数都会比之前插入的数都大,因此插完之后最长上升子序列的长度不会变化。

那么每个节点维护一个子树最长上升子序列的长度的最大值,插入时选一个前面的最长的最长上升子序列接上。

星系探索

思路

把这棵树转换成 dfs 序序列,然后对于三个操作:

  • 求 \(u\) 到根的权值和:求 \(u\) 在 dfs 序序列上的前缀和。
  • 改变 \(u\) 的父亲:移动 \(u\) 子树的 dfs 序序列代表的区间的位置。
  • 给 \(u\) 子树加一个定值:把 \(u\) 子树的 dfs 序序列代表的区间裂出来,打个标记再合并回去。

代码

点击查看代码
const ll N = 1e6+ 10;

namespace FHQ_TREAP {
class TreeNode {
public:
ll val, pn, rk, sz, so[2], ta, sum, sp, fa;
inline void Add (ll num, ll tmp) { sum = val = num * tmp, sp = pn = tmp, rk = rand (), sz = 1, so[0] = so[1] = 0; return; }
};
class FHQTreap {
private:
TreeNode tr[N];
ll tot = 0, rt = 0, a, b, c; #define va(p) tr[p].val
#define ta(p) tr[p].ta
#define rk(p) tr[p].rk
#define sz(p) tr[p].sz
#define pn(p) tr[p].pn
#define su(p) tr[p].sum
#define sp(p) tr[p].sp
#define fa(p) tr[p].fa
#define so(p, lr) tr[p].so[lr] inline ll NewP (ll num, ll pn) { tr[++tot].Add (num, pn); return tot; }
inline void PushUp (ll p) {
sz (p) = 1 + sz (so (p, 0)) + sz (so (p, 1));
su (p) = va (p) + su (so (p, 0)) + su (so (p, 1));
sp (p) = pn (p) + sp (so (p, 0)) + sp (so (p, 1));
fa (so (p, 0)) = fa (so (p, 1)) = p;
return;
}
inline void Tag (ll p, ll num) {
ta (p) += num;
va (p) += num * pn (p);
su (p) += num * sp (p);
return;
}
inline void PushDown (ll p) {
if (!ta (p)) return;
if (so (p, 0)) Tag (so (p, 0), ta (p));
if (so (p, 1)) Tag (so (p, 1), ta (p));
fa (so (p, 0)) = fa (so (p, 1)) = p;
ta (p) = 0;
return;
} inline void Split (ll p, ll x, ll& l, ll& r) {
if (!p) l = r = 0;
else {
PushDown (p);
if (sz (so (p, 0)) < x) l = p, Split (so (p, 1), x - sz (so (p, 0)) - 1, so (p, 1), r);
else r = p, Split (so (p, 0), x, l, so (p, 0));
PushUp (p);
}
return;
}
inline ll Merge (ll l, ll r) {
PushDown (l), PushDown (r);
if (!l || !r) return l + r;
if (rk (l) > rk (r)) {
so (l, 1) = Merge (so (l, 1), r);
PushUp (l);
return l;
}
else {
so (r, 0) = Merge (l, so (r, 0));
PushUp (r);
return r;
}
}
inline ll GetRank (ll x) {
ll cnt = sz (so (x, 0)) + 1;
for (ll p = x; fa (p); p = fa (p))
if (so (fa (p), 1) == p) cnt += sz (so (fa (p), 0)) + 1;
return cnt;
} public:
inline void Insert (ll x, ll p) {
rt = Merge (rt, NewP (x, p));
return;
}
inline void Modify (ll l, ll r, ll x) {
l = GetRank (l), r = GetRank (r);
Split (rt, r, a, b);
Split (a, l - 1, a, c);
Tag (c, x);
rt = Merge (Merge (a, c), b);
return;
}
inline void Move (ll l, ll r, ll x) {
l = GetRank (l), r = GetRank (r), x = GetRank (x);
if (x > r) x -= r - l + 1;
Split (rt, r, a, b);
Split (a, l - 1, a, c);
a = Merge (a, b);
Split (a, x, a, b);
rt = Merge (Merge (a, c), b);
return;
}
inline ll Query (ll x) {
x = GetRank (x);
Split (rt, x, a, b);
ll ans = su (a);
rt = Merge (a, b);
return ans;
} #undef va
#undef ta
#undef rk
#undef sz
#undef pn
#undef su
#undef sp
#undef fa
#undef so
};
} namespace SOLVE {
FHQ_TREAP::FHQTreap tr;
ll n, m, fa[N], w[N], cnt;
ll dfn[N], sec[N][2];
std::vector<ll> tu[N];
inline ll rnt () {
ll x = 0, w = 1; char c = getchar ();
while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
return x * w;
}
inline char rch () {
char c = getchar ();
while (c < 'A' || 'Z' < c) c = getchar ();
return c;
}
inline void Dfs (ll x) {
dfn[sec[x][0] = ++cnt] = x;
tr.Insert (w[x], 1);
far (i, tu[x]) Dfs (i);
sec[x][1] = ++cnt;
tr.Insert (w[x], -1);
return;
}
inline void In () {
n = rnt ();
_for (i, 2, n) {
fa[i] = rnt ();
tu[fa[i]].push_back (i);
}
_for (i, 1, n) w[i] = rnt ();
m = rnt ();
Dfs (1);
return;
}
inline void Out () {
while (m--) {
char opt = rch ();
if (opt == 'Q') {
ll d = rnt ();
printf ("%lld\n", tr.Query (sec[d][0]));
}
else if (opt == 'C') {
ll x = rnt (), y = rnt ();
tr.Move (sec[x][0], sec[x][1], sec[y][0]);
}
else {
ll x = rnt (), y = rnt ();
tr.Modify (sec[x][0], sec[x][1], y);
}
}
return;
}
}
\[\Huge\mathfrak{The End}
\]

「学习笔记」平衡树基础:Splay 和 Treap的更多相关文章

  1. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  2. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  3. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  4. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  5. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  6. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  7. 「学习笔记」 FHQ Treap

    FHQ Treap FHQ Treap (%%%发明者范浩强年年NOI金牌)是一种神奇的数据结构,也叫非旋Treap,它不像Treap zig zag搞不清楚(所以叫非旋嘛),也不像Splay完全看不 ...

  8. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  9. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

  10. 「学习笔记」Fast Fourier Transform

    前言 快速傅里叶变换(\(\text{Fast Fourier Transform,FFT}\) )是一种能在\(O(n \log n)\)的时间内完成多项式乘法的算法,在\(OI\)中的应用很多,是 ...

随机推荐

  1. memoのQt自动调整窗口尺寸

    折腾了好久,好久.终于搞出一个自认为还算可以的方案: QTimer::singleShot(0, this, [this]{ this->adjustSize(); }); 这个解决方案确实有点 ...

  2. 「SOL」NOI2016 Day1 解题报告

    第一次打 NOI,还是先把以前的 NOI 题刷一遍吧? # 目录 目录 # 目录 # A. 优秀的拆分 excellent # B. 网格 grid # C. 循环之美 cyclic # 小结 # 源 ...

  3. noi 1.1 3 对齐输出

    描述 输入三个整数,按每个整数占8个字符的宽度,右对齐输出它们. 输入 只有一行,包含三个整数,整数之间以一个空格分开. 输出 只有一行,按照格式要求依次输出三个整数,之间以一个空格分开. 样例输入 ...

  4. K Smallest In Unsorted Array

    Find the K smallest numbers in an unsorted integer array A. The returned numbers should be in ascend ...

  5. Vue3 流程图组件库 Vue Flow 简单使用

    官网 Vue Flow 官网 Vue Flow GitHub 安装 npm i --save @vue-flow/core yarn add @vue-flow/core pnpm i @vue-fl ...

  6. opencv实战之透视变换

    import cv2 import numpy as np import pytesseract def cv_show(imgname,img): cv2.imshow(imgname,img) c ...

  7. fpga 通过vga模块使电脑显示图片

    对vga模块搞了好几天,vga接口时序不难,就是行计数,列计数,对应到每一个像素上去.这里我先把vga控制模块的程序付上.采用的是小梅哥的程序.屏幕是800X600,配置的时钟是40M.注意改变屏幕尺 ...

  8. 正则url匹配

    今天来说一下正则的url匹配 示例:url ="https://v5.lairen.com/activity?id=862&code=ab9a61823398273b7b036fd9 ...

  9. NOIP2009普及组

    T3]细胞分裂 [算法]数论 [题解]均分的本质是A整除B,A整除B等价于A的质因数是B的子集. 1.将m1分解质因数,即m1=p1^a1*p2^a2*...*pk^ak 所以M=m1^m2=p1^( ...

  10. JS样式获取的封装方法

    样式获取 style属性 只能获取标签内容style属性里面存在的一些样式 如果你需要获取对应的全局所有地方设置样式 我们就需要采用一些方法 getComputedStyle 方法属于window的方 ...