平衡树学习笔记
不持续更新。
1 FHQ-Treap#
1.1 前置知识#
FHQ-Treap 一般使用小根堆。
1.2 FHQ-Treap 简述#
FHQ-Treap 是一种基于分裂和合并操作的平衡树。它没有旋转,极易上手,非常适合 cainiaoshanglu。
1.3 FHQ-Treap 核心思想#
我们对于一个点存储两个权值 , 其中 满足小根堆性质, 满足 BST 性质。 我们可以对于 进行随机赋值, 使得期望时间复杂度为 的.
FHQ-Treap 基于合并与分裂函数, 轻易的实现了 P3369 的六种功能, 即易懂又他妈的好写.
1.4 FHQ-Treap 基础操作#
1.4.1 更新操作#
用于更新节点信息改变后节点的值。
void pushUp(int x) {
t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
}
1.4.2 合并操作#
满足 ,即小根堆来合并.
发现由于之前将 分裂开了,所以 里面的所有值都大于 的。也就是说,我们只需要确定父子关系即可合并。
假设现在两棵树合并到 .
-
若当前 ,则显然 是 的右儿子。
-
若当前 ,则显然 是 的左儿子。
递归合并即可。
int merge(int x, int y) {
if (!x || !y) return x + y;
if (t[x].a < t[y].a) {
t[x].ch[1] = merge(t[x].ch[1], y);
pushUp(x);
return x;
} else {
t[y].ch[0] = merge(x, t[y].ch[0]);
pushUp(y);
return y;
}
}
实现细节:注意,这里的合并函数是默认了 点子树的所有权值小于等于 点子树的。
1.4.3 分裂操作#
我们通过 ,即 BST 来分裂。
假设我们要把 的分裂开来,那么假设现在走到 。
-
如果 ,那么将 及其右子树全部连到左树上,继续递归左儿子。
-
如果 ,那么将 及其左子树全部连到右树上,继续递归右儿子。
一般有两种分裂方式,一种是 按权值 ,一种是 按子树大小 。这里两种都放一下。
按照权值分裂:
void split(int now, int k, int &x, int &y) {
if (now == 0) return x = y = 0, void();
if (t[now].t >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
else y = now, split(t[now].ch[1], k, x, t[now].ch[1]);
pushUp(now);
}
按照大小分裂:
void split(int now, int k, int &x, int &y) {
if (now == 0) return x = y = 0, void();
if (k <= t[t[now].ch[0]].siz) x = now, split(t[now].ch[0], k, t[x].ch[0], y);
else y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[x].ch[1]);
pushUp(now);
}
1.5 FHQ-Treap 复合操作#
1.5.1 插入操作#
假设插入的权值为 。
我们先按照权值对 FHQ-Treap 进行分裂,把平衡树分成 和 的两部分。最后分别与新建点合并即可。
void Insert(int key) {
int p, q;
split(rt, key, p, q);
p = merge(newNode(key), p);
rt = merge(q, p);
}
1.5.2 删除操作#
我们考虑把平衡树分裂成 和 的两部分。
对于 的部分,我们再把他分裂成 和 的两部分。
对于 的部分,我们可以删除它的根 —— 把它的两个儿子合并。最后全部合在一起即可。
void Delete(int key) {
int p, q, o;
split(rt, key, p, q);
split(p, key + 1, p, o);
o = merge(t[o].ch[0], t[o].ch[1]);
p = merge(o, p);
rt = merge(q, p);
}
1.5.3 查询操作#
类似于线段树上二分,不多叙述。
int Query(int val) {
int res = 0, now = rt;
while (now) {
if (t[now].t >= val) now = t[now].ch[0];
else {
res += t[t[now].ch[0]].siz + 1;
now = t[now].ch[1];
}
}
return res + 1;
}
1.5.4 排名操作#
类似于线段树上二分,不多叙述。
int Rank(int x) {
int now = rt; x --;
while (now) {
if (t[t[now].ch[0]].siz > x) {
now = t[now].ch[0];
} else if (x == t[t[now].ch[0]].siz) {
return t[now].t;
} else {
x -= t[t[now].ch[0]].siz + 1;
now = t[now].ch[1];
}
}
return -1;
}
1.5.5 前缀查询#
我们把平衡树分裂成 和 ,在 的部分去暴力跑最小值即可。
int Precursor(int val) {
int p, q;
split(rt, val, p, q);
int x = q, res = -1;
while (x) {
res = t[x].t;
if (t[x].ch[1]) x = t[x].ch[1];
else break;
}
rt = merge(q, p);
return res;
}
1.5.6 后缀查询#
我们把平衡树分裂成 和 ,在 的部分去暴力跑最大值即可。
int Suffix(int val) {
int p, q;
split(rt, val + 1, p, q);
int x = p, res = -1;
while (x) {
res = t[x].t;
if (t[x].ch[0]) x = t[x].ch[0];
else break;
}
rt = merge(q, p);
return res;
}
1.6 FHQ-Treap 维护区间信息#
1.6.1 FHQ-Treap 维护区间思想#
因为在维护区间的时候,一般把权值的中序遍历视为这个序列当前的顺序,所以 一般情况下,维护区间信息的 FHQ 权值不满足小根堆。
于是在分裂时,我们就只能 按照大小分裂。
由于 FHQ-Treap 的分裂比较厉害,我们可以通过两次分裂轻松的把代表 的子树给裂出来。
但是如果直接整个子树更新,时间复杂度肯定爆炸。所以我们要考虑学习线段树,在平衡树上打 tag 即可。
以下是一个区间翻转的例子。
void Reverse(int l, int r) {
long long x, y, z;
split(rt, r, x, y);
split(y, l - 1, z, y);
t[z].rev ^= 1;
y = merge(y, z);
rt = merge(y, x);
}
1.6.2 维护区间信息需要注意的点#
- 一定要清空 号节点。由于在没有儿子的时候儿子变量存储的是 ,导致在 pushUp 的时候有可能会把 节点的信息(也就是本来没有的)给 pushUp 到正常节点上。
1.7 FHQ-Treap 经典例题#
I 序列终结者#
FHQ-Treap 板子题,就是单纯的区间操作。用来练一下手。
/*******************************
| Author: DE_aemmprty
| Problem: P4146 序列终结者
| Contest: Luogu
| URL: https://www.luogu.com.cn/problem/P4146
| When: 2024-04-03 19:10:26
|
| Memory: 128 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
long long read() {
char c = getchar();
long long x = 0, p = 1;
while ((c < '0' || c > '9') && c != '-') c = getchar();
if (c == '-') p = -1, c = getchar();
while (c >= '0' && c <= '9')
x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * p;
}
const int N = 5e4 + 7;
mt19937 rnd(time(0));
int n, m;
struct FHQ {
long long t, a, ch[2], val;
long long rev, add, siz, mx;
} t[N];
struct FHQ_Treap {
int cnt, rt;
void init() { cnt = 0, rt = 0; t[0].mx = -2e18;}
int newNode(int v) {
t[++ cnt] = {v, rnd(), {0, 0}, 0, 0, 0, 1, 0};
return cnt;
}
void pushUp(int x) {
t[x].rev = t[x].add = 0;
t[x].mx = max(t[x].val, max(t[t[x].ch[0]].mx, t[t[x].ch[1]].mx));
t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
}
void pushDown(int x) {
if (t[x].rev) {
t[t[x].ch[0]].rev ^= 1;
t[t[x].ch[1]].rev ^= 1;
swap(t[x].ch[0], t[x].ch[1]);
t[x].rev = 0;
}
t[t[x].ch[0]].add += t[x].add, t[t[x].ch[1]].add += t[x].add;
t[t[x].ch[0]].mx += t[x].add, t[t[x].ch[1]].mx += t[x].add;
t[t[x].ch[0]].val += t[x].add, t[t[x].ch[1]].val += t[x].add;
t[x].add = 0;
}
int merge(int x, int y) {
if (!x || !y) return x + y;
pushDown(x), pushDown(y);
if (t[x].a < t[y].a) {
t[x].ch[1] = merge(t[x].ch[1], y);
pushUp(x);
return x;
}
else {
t[y].ch[0] = merge(x, t[y].ch[0]);
pushUp(y);
return y;
}
}
void split(int now, int k, long long &x, long long &y) {
if (!now) return x = y = 0, void();
pushDown(now);
if (t[t[now].ch[0]].siz >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
else y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[now].ch[1]);
pushUp(now);
}
void Insert(int v) {
long long x, y;
split(rt, v - 1, x, y);
y = merge(y, newNode(v));
rt = merge(y, x);
}
void Update(int l, int r, long long v) {
long long x, y, z;
split(rt, r, x, y);
split(y, l - 1, z, y);
t[z].add += v, t[z].val += v, t[z].mx += v;
y = merge(y, z);
rt = merge(y, x);
}
void Reverse(int l, int r) {
long long x, y, z;
split(rt, r, x, y);
split(y, l - 1, z, y);
t[z].rev ^= 1;
y = merge(y, z);
rt = merge(y, x);
}
long long getMax(int l, int r) {
long long x, y, z;
split(rt, r, x, y);
split(y, l - 1, z, y);
long long res = t[z].mx;
y = merge(y, z);
rt = merge(y, x);
return res;
}
} F;
void solve() {
n = read(), m = read();
F.init();
for (int i = 1; i <= n; i ++)
F.Insert(i);
while (m --) {
long long op = read(), l, r, v;
if (op == 1) {
l = read(), r = read(), v = read();
F.Update(l, r, v);
} else if (op == 2) {
l = read(), r = read();
F.Reverse(l, r);
} else {
l = read(), r = read();
cout << F.getMax(l, r) << '\n';
}
}
}
signed main() {
int t = 1;
while (t --) solve();
return 0;
}
II Peaks#
我们把询问先离线下来,然后从下往上扫。
使用并查集维护连通块关系,使用非旋 Treap 来进行第 大的维护。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
int n, m, q;
struct Edge {
int u, v, w;
bool operator < (const Edge &x) const {
return w < x.w;
}
} e[N];
long long ans[N], h[N];
struct Query {
int v, x, k, id;
bool operator < (const Query &qwq) const {
return x < qwq.x;
}
} p[N];
namespace DSU {
int fa[N];
void init() { for (int i = 1; i <= n; i ++) fa[i] = i;}
int find(int x) { return x == fa[x] ? x : (fa[x] = find(fa[x]));}
void Union(int i, int j) { fa[find(j)] = find(i);}
} using namespace DSU;
long long read() {
long long x = 0, k = 1; char c = getchar();
while ((c < '0' || c > '9') && c != '-') c = getchar();
if (c == '-') k = -1, c = getchar();
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * k;
}
mt19937 rnd(time(0));
struct FHQ {
int ch[2];
long long a;
int t, siz;
} t[N];
int cnt;
namespace FHQ_Treap {
int newNode(int v) { t[++ cnt] = {{0, 0}, (long long) rnd(), v, 1}; return cnt;}
void pushUp(int x) { t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;}
int merge(int x, int y) {
if (!x || !y) return x + y;
if (t[x].a < t[y].a) {
t[x].ch[1] = merge(t[x].ch[1], y);
pushUp(x);
return x;
} else {
t[y].ch[0] = merge(x, t[y].ch[0]);
pushUp(y);
return y;
}
}
void split(int now, int k, int &x, int &y) {
if (!now) return x = y = 0, void();
if (t[now].t < k) {
x = now;
split(t[now].ch[1], k, t[now].ch[1], y);
} else {
y = now;
split(t[now].ch[0], k, x, t[now].ch[0]);
}
pushUp(now);
}
int insert(int now, int v) {
int x, y;
split(now, h[v], x, y);
x = merge(x, merge(v, y));
if (x == v) Union(v, now);
else Union(now, v);
return x;
}
int kth(int now, int k) {
while (now) {
if (k <= t[t[now].ch[1]].siz) now = t[now].ch[1];
else if (k == t[t[now].ch[1]].siz + 1) return t[now].t;
else k -= t[t[now].ch[1]].siz + 1, now = t[now].ch[0];
}
return -1;
}
} using namespace FHQ_Treap;
void Merge(int p, int q) {
int rp = find(p), rq = find(q);
if (rp == rq) return ;
if (t[rp].siz > t[rq].siz) swap(rp, rq);
int siz = t[rp].siz;
while (siz) {
int lx = t[rp].ch[0], rx = t[rp].ch[1];
t[rp].ch[0] = t[rp].ch[1] = 0; t[rp].siz = 1;
rq = insert(rq, rp);
rp = merge(lx, rx);
siz --;
}
}
signed main() {
n = read(), m = read(), q = read();
init();
for (int i = 1; i <= n; i ++) h[i] = read();
for (int i = 1; i <= m; i ++)
e[i].u = read(), e[i].v = read(), e[i].w = read();
sort(e + 1, e + m + 1);
for (int i = 1; i <= q; i ++)
p[i].v = read(), p[i].x = read(), p[i].k = read(), p[i].id = i;
for (int i = 1; i <= n; i ++) newNode(h[i]);
sort(p + 1, p + q + 1);
for (int i = 1, j = 1; i <= q; i ++) {
while (j <= m && e[j].w <= p[i].x)
Merge(e[j].u, e[j].v), j ++;
ans[p[i].id] = kth(find(p[i].v), p[i].k);
}
for (int i = 1; i <= q; i ++)
cout << ans[i] << '\n';
return 0;
}
III 队列#
IV 火星人 prefix#
对于后缀求 LCP,较简单的求法有后缀数组,后缀自动机和二分加哈希。
容易发现,后缀数组和后缀自动机比较难以进行修改。遂进行二分加哈希。
我们可以维护这一个字符串的前缀哈希值。接下来,考虑如何维护:
-
对于修改操作,本质上相当于对一个后缀进行 区间加法。
-
对于插入操作,本质上相当于对一个后缀进行 区间加法,区间乘法(整体偏移) 和 插入。
-
对于查询操作,本质上相当于查询 的哈希值然后进行二分。
总结一下,我们发现这个数据结构需要做到 区间操作 和 插入。非旋 Treap 可以做到这一点。
// 咕。
作者:DE_aemmprty
出处:https://www.cnblogs.com/aemmprty/p/18104830
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂